This commit is contained in:
jamesfalkner
2019-07-15 13:16:49 -04:00
parent 0ceb07bea7
commit f99157c2f0
8 changed files with 1834 additions and 150 deletions

View File

@@ -30,6 +30,4 @@ modules:
tracing:
name: Tracing Quarkus Apps
security:
name: Securing Resources with Keycloak
extra:
name: Extra Credit
name: Securing Quarkus APIs

View File

@@ -27,5 +27,4 @@ modules:
- kafka
- monitoring
- tracing
- security
- extra
- security

View File

@@ -156,8 +156,8 @@ Earlier you implemented a series of MicroProfile health checks. To make OpenShif
[source,sh,role="copypaste"]
----
oc set probe dc/people --readiness --get-url=http://:8080/health/ready
oc set probe dc/people --liveness --get-url=http://:8080/health/live
oc set probe dc/people --readiness --initial-delay-seconds=30 --get-url=http://:8080/health/ready
oc set probe dc/people --liveness --initial-delay-seconds=30 --get-url=http://:8080/health/live
----
This configures both a _readiness_ probe (is the app initialized and ready to serve requests?) and a _liveness_ probe (is the app still up and ready to serve requests) with default timeouts. OpenShift will not route any traffic to pods that don't respond successfully to these probes. By editing these, it will trigger a new deployment so make sure the app comes up with its new probes in place:

View File

@@ -1,2 +0,0 @@
## Extra Credit

BIN
docs/images/secapis.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

View File

@@ -1,100 +1,42 @@
## Securing Quarkus APIs with Keycloak
## Securing Quarkus APIs
This exercise demonstrates how your Quarkus applications can use https://keyloak.org[Keycloak] to protect your JAX-RS applications using _bearer token authorization_, where these tokens are issued by a Keycloak Server.
Bearer Token Authorization is the process of authorizing HTTP requests based on the existence and validity of a bearer token representing a subject and her access context, where the token provides valuable information to determine the subject of the call as well whether or not a HTTP resource can be accessed. This is commonly used in OAuth-based identity and access management systems like https://keycloak.org[Keycloak], a popular open source project. In this exercise we'll show you how to use https://github.com/eclipse/microprofile-jwt-auth/releases/download/1.1.1/microprofile-jwt-auth-spec.pdf[Microprofile JSON Web Token (JWT) RBAC], https://keyloak.org[Keycloak] and https://en.wikipedia.org/wiki/OAuth[OAuth] in your Quarkus applications.
Bearer Token Authorization is the process of authorizing HTTP requests based on the existence and validity of a bearer token representing a subject and her access context, where the token provides valuable information to determine the subject of the call as well whether or not a HTTP resource can be accessed.
## Add Keycloak + JWT to Quarkus
Keycloak is a OAuth 2.0 compliant Authorization Server, capable of issuing access tokens so that you can use them to access protected resources. We are not going to enter into the details on what OAuth 2.0 is and how it works but give you a guideline on how to use OAuth 2.0 in your JAX-RS applications using the Quarkus Keycloak Extension.
If you are already familiar with Keycloak, youll notice that the extension is basically another adapter implementation but specific for Quarkus applications. Otherwise, you can find more information in Keycloak documentation.
## Add Keycloak to Quarkus
Like other exercises, we'll need another extension to enable the use of Keycloak. Install it with:
Like other exercises, we'll need another extension to enable the use of Keycloak and MicroProfile JWT. Install them with:
[source,sh,role="copypaste"]
----
mvn quarkus:add-extension -Dextensions="keycloak"
mvn quarkus:add-extension -Dextensions="keycloak, jwt"
----
This will add the necessary entries in your `pom.xml` to bring in the Keycloak extension which is an implementation of a Keycloak Adapter for Quarkus applications and provides all the necessary capabilities to integrate with a Keycloak Server and perform bearer token authorization.
This will add the necessary entries in your `pom.xml` to bring in the Keycloak and JWT extensions.
## Create secured endpoints
## Configure Quarkus for MicroProfile JWT
Create a new class in the `org.acme.people.rest` package called `SecuredResource` with the following code which will create **three new secured endpoints** all beginning with `/secured`:
[source,java,role="copypaste"]
----
package org.acme.people.rest;
import javax.annotation.security.RolesAllowed;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.keycloak.KeycloakSecurityContext;
@Path("/secured")
public class SecuredResource {
@Inject
KeycloakSecurityContext keycloakSecurityContext; // <1>
@GET
@Path("/me") // <2>
@RolesAllowed("user")
@Produces(MediaType.APPLICATION_JSON)
@NoCache
public User me() {
return new User(keycloakSecurityContext);
}
@GET
@Path("/admin") // <3>
@RolesAllowed("admin")
@Produces(MediaType.TEXT_PLAIN)
public String admin() {
return "granted";
}
@GET
@Path("/confidential") // <4>
@Produces(MediaType.TEXT_PLAIN)
public String confidential() {
return "confidential";
}
public class User {
private final String userName;
User(KeycloakSecurityContext securityContext) {
this.userName = securityContext.getToken().getPreferredUsername();
}
public String getUserName() {
return userName;
}
}
}
----
<1> The `KeycloakSecurityContext` is an object produced by the Keycloak extension that you can use to obtain information from tokens sent to your application. In the source code above we are using this object to access the token representation and obtain the username of the user represented by the token.
<2> Here we are using a `@RolesAllowed` annotation for the `/secured/me` endpoint to make sure that only users granted with the `user` role (i.e. are logged in) can access the endpoint.
<3> For the `/secured/admin` we only want administrators (those granted the `admin` role in our identity management system) to be able to access
<4> For the `/api/confidential` there is no explicit access control defined to this endpoint. The Keycloak extension will enforce access to this endpoint based on the policies defined in the Keycloak Server. For now, dont worry about how the extension enforces access to `/api/confidential`. Just keep in mind that there is some configuration that we need to define to make this happen.
## Configuring using the application.properties file
The Keycloak extension allows you to define the adapter configuration using either the `application.properties` file or using a `keycloak.json`. Open up your `application.properties` file and add the following configuration for Keycloak:
Some configuration of the extensions is required. Add this to your `application.properties`:
[source,none,role="copypaste"]
----
quarkus.keycloak.realm=quarkus # <1>
quarkus.keycloak.auth-server-url=http://{{ KEYCLOAK_URL }}/auth
mp.jwt.verify.publickey.location=META-INF/resources/publicKey.pem # <1>
mp.jwt.verify.issuer=https://quarkus.io/using-jwt-rbac # <2>
quarkus.smallrye-jwt.auth-mechanism=MP-JWT # <3>
quarkus.smallrye-jwt.realm-name=quarkus
----
<1> Sets public key location for JWT authentication. This file has been created for you.
<2> Issuer URL. This must match the incoming JWT `iss` _claims_ or else authentication fails.
<3> Sets authentication mechanism name to `MP-JWT`, the MicroProfile JWT RBAC specification standard name for the token based authentication mechanism.
## Configuring Keycloak
Next, add these to your `application.properties` for Keycloak:
[source,none,role="copypaste"]
----
quarkus.keycloak.realm=quarkus
quarkus.keycloak.auth-server-url={{ KEYCLOAK_URL }}/auth
quarkus.keycloak.resource=backend-service
quarkus.keycloak.bearer-only=true
quarkus.keycloak.credentials.secret=secret
@@ -110,9 +52,118 @@ We are using the same Keycloak instance that we use for Eclipse Che, and have pr
For more details about this file and all the supported options, please take a look at https://www.keycloak.org/docs/latest/securing_apps/index.html#_java_adapter_config[Keycloak Adapter Config].
## Create secured endpoints
Create a new class in the `org.acme.people.rest` package called `SecuredResource` with the following code which will create **four new secured endpoints** all beginning with `/secured`:
[source,java,role="copypaste"]
----
package org.acme.people.rest;
import java.security.Principal;
import java.util.Optional;
import javax.annotation.security.RolesAllowed;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.json.JsonString;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.SecurityContext;
import org.eclipse.microprofile.jwt.Claim;
import org.eclipse.microprofile.jwt.Claims;
import org.eclipse.microprofile.jwt.JsonWebToken;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.keycloak.KeycloakSecurityContext;
@Path("/secured")
@RequestScoped // <1>
public class SecuredResource {
@Inject
KeycloakSecurityContext keycloakSecurityContext; // <2>
@Inject
JsonWebToken jwt; // <3>
@Inject
@Claim(standard = Claims.iss)
Optional<JsonString> issuer; // <4>
@GET
@Path("/me/jwt")
@PermitAll
@Produces(MediaType.TEXT_PLAIN)
public String meJwt(@Context SecurityContext ctx) { // <5>
Principal caller = ctx.getUserPrincipal();
String name = caller == null ? "anonymous" : caller.getName();
boolean hasJWT = jwt != null;
final StringBuilder helloReply = new StringBuilder(String.format("hello %s, isSecure: %s, authScheme: %s, hasJWT: %s\n", name, ctx.isSecure(), ctx.getAuthenticationScheme(), hasJWT));
if (hasJWT && (jwt.getClaimNames() != null)) {
helloReply.append("Injected issuer: [" + issuer.get() + "]\n");
jwt.getClaimNames().forEach(n -> {
helloReply.append("\nClaim Name: [" + n + "] Claim Value: [" + jwt.getClaim(n) + "]");
});
}
return helloReply.toString();
}
@GET
@Path("/me") // <5>
@RolesAllowed("user")
@Produces(MediaType.APPLICATION_JSON)
@NoCache
public User me() {
return new User(keycloakSecurityContext);
}
@GET
@Path("/admin") // <6>
@RolesAllowed("admin")
@Produces(MediaType.TEXT_PLAIN)
public String admin() {
return "granted";
}
@GET
@Path("/confidential") // <7>
@Produces(MediaType.TEXT_PLAIN)
public String confidential() {
return "confidential";
}
public class User { // <8>
private final String userName;
User(KeycloakSecurityContext securityContext) {
this.userName = securityContext.getToken().getPreferredUsername();
}
public String getUserName() {
return userName;
}
}
}
----
<1> Adds a `@RequestScoped` as Quarkus uses a default scoping of `ApplicationScoped` and this will produce undesirable behavior since JWT claims are naturally request scoped.
<2> The `KeycloakSecurityContext` is an object produced by the Keycloak extension that you can use to obtain information from tokens sent to your application.
<3> `@JsonWebToken` provides access to the claims associated with the current authenticated JWT token.
<4> When using JWT Authentication, claims encoded in tokens can be `@Inject` ed into your class for convenient access.
<5> The `/me` and `/me/jwt` endpoints demonstrate how to access the security context for Quarkus apps secured with JWT or Keycloak. In the first one we are using a `@RolesAllowed` annotation to make sure that only users granted with the `user` role (i.e. are logged in) can access the endpoint. The `/me/jwt` shows how to access claims.
<6> For the `/secured/admin` we only want administrators (those granted the `admin` role in our identity management system) to be able to access
<7> For the `/api/confidential` there is no explicit access control defined to this endpoint. The Keycloak extension will enforce access to this endpoint based on the policies defined in the Keycloak Server. For now, dont worry about how the extension enforces access to `/api/confidential`. Just keep in mind that there is some configuration that we need to define to make this happen.
<8> Simple POJO to encapsulate the data model of a Keycloak user.
## Rebuild and redeploy app
First, re-build the app using the command palette and selecting **Build Executable JAR**. Once that's done, run the following command to re-deploy:
First, re-build the app using the command palette and selecting **Create Executable JAR**. Once that's done, run the following command to re-deploy:
[source,sh,role="copypaste"]
----
@@ -139,51 +190,47 @@ Get a token for user `alice` with this command:
[source,sh,role="copypaste"]
----
export ALICE_TOKEN=$(\
curl -X POST http://{{KEYCLOAK_URL}}/auth/realms/quarkus/protocol/openid-connect/token \
curl -X POST {{KEYCLOAK_URL}}/auth/realms/quarkus/protocol/openid-connect/token \
--user backend-service:secret \
-H 'content-type: application/x-www-form-urlencoded' \
-d 'username=alice&password=alice&grant_type=password' | jq --raw-output '.access_token' \
)
echo $ALICE_TOKEN
----
This issues a `curl` command to Keycloak (using `backend-service` credentials which is a special user that is allowed acess to the Keycloak REST API), and fetches a token for Alice using their credentials. You can see the value of this token:
[source,sh,role="copypaste"]
----
echo $ALICE_TOKEN
----
This issues a `curl` command to Keycloak (using `backend-service` credentials which is a special user that is allowed acess to the Keycloak REST API), and fetches a token for Alice using their credentials.
Any user is allowed to access the `/secured/me` endpoint which basically returns a JSON payload with personal details about the user that's part of the `KeycloakSecurityContext` object.
[NOTE]
====
There are other APIs you can use if you try to auto-complete the method name using Che, e.g. `getBirthDate()` or `getPicture()`. Place the cursor just after `securityContext.getToken().` and press CTRL-SPACE to see them:
::img
====
Try out the secured API as Alice:
Try out the Keycloak-secured API as Alice:
[source,sh,role="copypaste"]
----
curl -v -X GET \
curl -X GET \
http://$(oc get route people -o=go-template --template='{{ .spec.host }}')/secured/me \
-H "Authorization: Bearer $ALICE_TOKEN"
----
You should see:
[source,none]
[source,json]
----
TODO: OUTPUT
{"userName":"alice"}
----
[NOTE]
====
There are other APIs you can use if you try to auto-complete the method name using Che, e.g. `getBirthDate()` or `getPicture()`. Place the cursor just after `securityContext.getToken().get` and press CTRL-SPACE to see them:
image::secapis.png[apis, 800]
====
### Test Admin
The `/secured/admin` endpoint can only be accessed by users with the `admin` role. If you try to access this endpoint with the previously issued access token, you should get a 403 response from the server. Try it:
[source,sh,role="copypaste"]
----
curl -v -X GET \
curl -X GET \
http://$(oc get route people -o=go-template --template='{{ .spec.host }}')/secured/admin \
-H "Authorization: Bearer $ALICE_TOKEN"
----
@@ -192,7 +239,7 @@ You should see:
[source,none]
----
TODO: OUTPUT
Access forbidden: role not allowed
----
Failed as expected! Obtain an Admin token:
@@ -200,7 +247,7 @@ Failed as expected! Obtain an Admin token:
[source,sh,role="copypaste"]
----
export ADMIN_TOKEN=$(\
curl -X POST http://{{KEYCLOAK_URL}}/auth/realms/quarkus/protocol/openid-connect/token \
curl -X POST {{KEYCLOAK_URL}}/auth/realms/quarkus/protocol/openid-connect/token \
--user backend-service:secret \
-H 'content-type: application/x-www-form-urlencoded' \
-d 'username=admin&password=admin&grant_type=password' | jq --raw-output '.access_token' \
@@ -208,9 +255,10 @@ export ADMIN_TOKEN=$(\
----
And try again with your new token:
[source,sh,role="copypaste"]
----
curl -v -X GET \
curl -X GET \
http://$(oc get route people -o=go-template --template='{{ .spec.host }}')/secured/admin \
-H "Authorization: Bearer $ADMIN_TOKEN"
----
@@ -219,7 +267,7 @@ You should see:
[source,none]
----
TODO: OUTPUT
granted
----
Success!
@@ -237,11 +285,14 @@ curl -v -X GET \
-H "Authorization: Bearer $ADMIN_TOKEN"
----
You should see:
You should see in the returned HTTP headers:
[source,none]
----
TODO: output
< HTTP/1.1 403 Forbidden
< Content-Length: 0
< Date: Mon, 15 Jul 2019 14:13:27 GMT
< Set-Cookie: 199a0e26f45fa42c8974157b896962e3=d0ea1fac5248f71f70eee9941b4902f1; path=/; HttpOnly
----
Failed as expected!
@@ -251,7 +302,7 @@ To access the confidential endpoint, you should obtain an access token for user
[source,sh,role="copypaste"]
----
export JDOE_TOKEN=$(\
curl -X POST http://{{KEYCLOAK_URL}}/auth/realms/quarkus/protocol/openid-connect/token \
curl -X POST {{KEYCLOAK_URL}}/auth/realms/quarkus/protocol/openid-connect/token \
--user backend-service:secret \
-H 'content-type: application/x-www-form-urlencoded' \
-d 'username=jdoe&password=jdoe&grant_type=password' | jq --raw-output '.access_token' \
@@ -262,7 +313,7 @@ And access the confidential endpoint with your new token:
[source,sh,role="copypaste"]
----
curl -v -X GET \
curl -X GET \
http://$(oc get route people -o=go-template --template='{{ .spec.host }}')/secured/confidential \
-H "Authorization: Bearer $JDOE_TOKEN"
----
@@ -271,28 +322,52 @@ You should see:
[source,none]
----
TODO: OUTPUT
confidential
----
Success!
Success! Even though our code did not explicitly protect the `/secured/confidential` endpoint, we can protect arbitrary URLs in Quarkus apps when using Keycloak and MicroProfile JWT.
### Test JWT Authentication
Use Alice's token to access the JWT endpoint:
[source,sh,role="copypaste"]
----
curl -X GET \
http://$(oc get route people -o=go-template --template='{{ .spec.host }}')/secured/me/jwt \
-H "Authorization: Bearer $ALICE_TOKEN"
----
You should see:
[source,none]
----
hello alice, isSecure: false, authScheme: MP-JWT, hasJWT: true
Injected issuer: ["http://keycloak-che.apps.cluster-orlando-c811.orlando-c811.openshiftworkshop.com/auth/realms/quarkus"]
Claim Name: [sub] Claim Value: [eb4123a3-b722-4798-9af5-8957f823657a]
Claim Name: [email_verified] Claim Value: [false]
Claim Name: [raw_token] Claim Value: [omitted][
Claim Name: [iss] Claim Value: [http://keycloak-che.apps.cluster-orlando-c811.orlando-c811.openshiftworkshop.com/auth/realms/quarkus]
Claim Name: [groups] Claim Value: [[]]
Claim Name: [typ] Claim Value: ["Bearer"]
Claim Name: [preferred_username] Claim Value: [alice]
Claim Name: [acr] Claim Value: [1]
Claim Name: [nbf] Claim Value: [0]
Claim Name: [realm_access] Claim Value: [{"roles":["user"]}]
Claim Name: [azp] Claim Value: [backend-service]
Claim Name: [auth_time] Claim Value: [0]
Claim Name: [scope] Claim Value: ["email profile"]
Claim Name: [exp] Claim Value: [1563210121]
Claim Name: [session_state] Claim Value: ["816e22c9-5dcb-4b8a-b90f-005b25e145e1"]
Claim Name: [iat] Claim Value: [1563209821]
Claim Name: [jti] Claim Value: [5010b2eb-bb49-4f25-94e4-309d87a041b4]
----
As you can see, the JSON Web Token (generated via Keycloak) was used to authenticate Alice, and show all of the various claims encoded in the JWT Token. You are also able to access claims using standard `@Inject` fields which are populated with claims when a request comes in (it can be seen above in the `Injected issuer` line near the beginning).
You can try it again using other users' tokens (just run the same `curl` again and replace `ALICE_TOKEN` with `JDOE_TOKEN` or `ADMIN_TOKEN` which you set up earlier).
## Congratulations!
This exercise demonstrated how your Quarkus application can use Keycloak to protect your JAX-RS applications using bearer token authorization, where these tokens are issued by a Keycloak Server.
Quarkus has a number of other security-related features, such as:
* JSON Web Token support for Access Control - Quarkus application can utilize the https://microprofile.io/project/eclipse/microprofile-jwt-auth[MicroProfile JWT RBAC] to provide secured access to the JAX-RS endpoints. See https://quarkus.io/guides/jwt-guide[this guide] for more detail.
* Quarkus comes with integration with the https://docs.jboss.org/author/display/WFLY/WildFly+Elytron+Security[Elytron security subsystem] to allow for RBAC based on the common security annotations `@RolesAllowed`, `@DenyAll`, `@PermitAll` on REST endpoints. See https://quarkus.io/guides/security-guide[this guide] for details.
This exercise demonstrated how your Quarkus application can use MicroProfile JWT in conjunction with Keycloak to protect your JAX-RS applications using JWT claims and bearer token authorization.

1619
files/quarkus-realm.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -252,18 +252,13 @@ SSO_TOKEN=$(curl -s -d "username=${KEYCLOAK_USER}&password=${KEYCLOAK_PASSWORD}&
-X POST http://keycloak-che.${HOSTNAME_SUFFIX}/auth/realms/master/protocol/openid-connect/token | \
jq -r '.access_token')
# Import realm from
# https://raw.githubusercontent.com/quarkusio/quarkus-quickstarts/master/using-keycloak/config/quarkus-realm.json
TMPREALM=$(mktemp)
curl -s -o $TMPREALM https://raw.githubusercontent.com/quarkusio/quarkus-quickstarts/master/using-keycloak/config/quarkus-realm.json
curl -v -H "Authorization: Bearer ${SSO_TOKEN}" -H "Content-Type:application/json" -d @${TMPREALM} \
# Import realm
curl -v -H "Authorization: Bearer ${SSO_TOKEN}" -H "Content-Type:application/json" -d ../files/quarkus-realm.json \
-X POST http://keycloak-che.${HOSTNAME_SUFFIX}/auth/admin/realms
rm -f ${TMPREALM}
# Create Che users
# Create Che users, let them view che namespace
for i in {1..$USERCOUNT} ; do
oc adm policy add-role-to-user view user${i} -n che
USERNAME=user${i}
FIRSTNAME=User${i}
LASTNAME=Developer