Implementierung des OAuth 2.0-Autorisierungsframeworks mit Jakarta EE

1. Übersicht

In diesem Tutorial stellen wir eine Implementierung für das OAuth 2.0 Authorization Framework mit Jakarta EE und MicroProfile bereit. Am wichtigsten ist, dass wir die Interaktion der OAuth 2.0-Rollen über den Grant-Typ des Autorisierungscodes implementieren. Die Motivation hinter diesem Schreiben besteht darin, Projekte zu unterstützen, die mit Jakarta EE umgesetzt werden, da dies OAuth noch nicht unterstützt.

Für die wichtigste Rolle, den Autorisierungsserver, implementieren wir den Autorisierungsendpunkt, den Token-Endpunkt und zusätzlich den JWK-Schlüsselendpunkt , der für den Ressourcenserver nützlich ist, um den öffentlichen Schlüssel abzurufen.

Da die Implementierung für eine schnelle Einrichtung einfach und unkompliziert sein soll, verwenden wir einen vorregistrierten Speicher für Clients und Benutzer und natürlich einen JWT-Speicher für Zugriffstoken.

Bevor Sie direkt zum Thema springen, ist es wichtig zu beachten, dass das Beispiel in diesem Tutorial zu Bildungszwecken dient. Für Produktionssysteme wird dringend empfohlen, eine ausgereifte, gut getestete Lösung wie Keycloak zu verwenden.

2. OAuth 2.0-Übersicht

In diesem Abschnitt geben wir einen kurzen Überblick über die OAuth 2.0-Rollen und den Grant Flow für den Autorisierungscode.

2.1. Rollen

Das OAuth 2.0-Framework impliziert die Zusammenarbeit zwischen den vier folgenden Rollen:

  • Ressourcenbesitzer : Normalerweise ist dies der Endbenutzer - es ist die Entität, die über einige Ressourcen verfügt, die es wert sind, geschützt zu werden
  • Ressourcenserver : Ein Service, der die Daten des Ressourcenbesitzers schützt und diese normalerweise über eine REST-API veröffentlicht
  • Client : Eine Anwendung, die die Daten des Ressourcenbesitzers verwendet
  • Authorization Server : Eine Anwendung, die Clients Berechtigungen oder Berechtigungen in Form ablaufender Token erteilt

2.2. Arten von Autorisierungszuschüssen

Ein Grant-Typ gibt an, wie ein Client die Berechtigung erhält, die Daten des Ressourcenbesitzers zu verwenden, letztendlich in Form eines Zugriffstokens.

Natürlich bevorzugen verschiedene Arten von Kunden verschiedene Arten von Zuschüssen:

  • Autorisierungscode : Am häufigsten bevorzugt- ob es sich um eine Webanwendung, eine native Anwendung oder eine einseitige Anwendung handelt , obwohl native und einseitige Apps einen zusätzlichen Schutz namens PKCE erfordern
  • Token aktualisieren : Ein spezieller Erneuerungszuschuss, der für Webanwendungen geeignet ist , um das vorhandene Token zu erneuern
  • Client-Anmeldeinformationen : Wird für die Service-zu-Service-Kommunikation bevorzugt, z. B. wenn der Ressourcenbesitzer kein Endbenutzer ist
  • Ressource Besitzer Passwort : Bevorzugt für die First-Party - Authentifizierung von nativen Anwendungen , sagen , wenn die mobilen App benötigen eine eigene Login - Seite

Darüber hinaus kann der Client den impliziten Grant-Typ verwenden. In der Regel ist es jedoch sicherer, den Berechtigungscode Grant mit PKCE zu verwenden.

2.3. Berechtigungscode Grant Flow

Da der Berechtigungscode-Grant-Flow am häufigsten verwendet wird, überprüfen wir auch, wie dies funktioniert. Genau das werden wir in diesem Lernprogramm erstellen.

Eine Anwendung - ein Client - fordert die Erlaubnis an, indem sie zum Endpunkt des Autorisierungsservers / authorize umleitet . Zu diesem Endpunkt gibt die Anwendung einen Rückrufendpunkt .

Der Autorisierungsserver bittet normalerweise den Endbenutzer - den Ressourcenbesitzer - um Erlaubnis. Wenn der Endbenutzer die Berechtigung erteilt, leitet der Autorisierungsserver mit einem Code zum Rückruf zurück .

Die Anwendung empfängt diesen Code und ruft dann authentifiziert den Endpunkt des Autorisierungsservers / Token an . Mit "authentifiziert" meinen wir, dass die Anwendung im Rahmen dieses Aufrufs beweist, wer sie ist. Wenn alle in der richtigen Reihenfolge angezeigt werden, antwortet der Autorisierungsserver mit dem Token.

Mit dem vorliegenden Token sendet die Anwendung ihre Anforderung an die API - den Ressourcenserver - und diese API überprüft das Token. Er kann den Autorisierungsserver auffordern, das Token mithilfe seines Endpunkts / introspect zu überprüfen . Wenn das Token in sich geschlossen ist, kann der Ressourcenserver die Optimierung durchführen, indem er die Signatur des Tokens lokal überprüft, wie dies bei JWT der Fall ist.

2.4. Was unterstützt Jakarta EE?

Noch nicht viel. In diesem Tutorial werden wir die meisten Dinge von Grund auf neu erstellen.

3. OAuth 2.0 Authorization Server

In dieser Implementierung konzentrieren wir uns auf den am häufigsten verwendeten Grant-Typ : Authorization Code.

3.1. Kunden- und Benutzerregistrierung

Ein Autorisierungsserver muss natürlich die Clients und Benutzer kennen, bevor er ihre Anforderungen autorisieren kann. Und es ist üblich, dass ein Autorisierungsserver eine Benutzeroberfläche dafür hat.

Der Einfachheit halber verwenden wir jedoch einen vorkonfigurierten Client:

INSERT INTO clients (client_id, client_secret, redirect_uri, scope, authorized_grant_types) VALUES ('webappclient', 'webappclientsecret', '//localhost:9180/callback', 'resource.read resource.write', 'authorization_code refresh_token');
@Entity @Table(name = "clients") public class Client { @Id @Column(name = "client_id") private String clientId; @Column(name = "client_secret") private String clientSecret; @Column(name = "redirect_uri") private String redirectUri; @Column(name = "scope") private String scope; // ... }

Und ein vorkonfigurierter Benutzer:

INSERT INTO users (user_id, password, roles, scopes) VALUES ('appuser', 'appusersecret', 'USER', 'resource.read resource.write');
@Entity @Table(name = "users") public class User implements Principal { @Id @Column(name = "user_id") private String userId; @Column(name = "password") private String password; @Column(name = "roles") private String roles; @Column(name = "scopes") private String scopes; // ... }

Beachten Sie, dass wir für dieses Lernprogramm Kennwörter im Klartext verwendet haben, diese jedoch in einer Produktionsumgebung gehasht werden sollten .

Im weiteren Verlauf dieses Tutorials zeigen wir, wie der Benutzer - der Ressourcenbesitzer - durch Implementierung des Autorisierungscodes Zugriff auf den Webapplienten - die Anwendung - gewähren kann .

3.2. Autorisierungsendpunkt

The main role of the authorization endpoint is to first authenticate the user and then ask for the permissions – or scopes – that the application wants.

As instructed by the OAuth2 specs, this endpoint should support the HTTP GET method, although it can also support the HTTP POST method. In this implementation, we'll support only the HTTP GET method.

First, the authorization endpoint requires that the user be authenticated. The spec doesn't require a certain way here, so let's use Form Authentication from the Jakarta EE 8 Security API:

@FormAuthenticationMechanismDefinition( loginToContinue = @LoginToContinue(loginPage = "/login.jsp", errorPage = "/login.jsp") )

The user will be redirected to /login.jsp for authentication and then will be available as a CallerPrincipal through the SecurityContext API:

Principal principal = securityContext.getCallerPrincipal();

We can put these together using JAX-RS:

@FormAuthenticationMechanismDefinition( loginToContinue = @LoginToContinue(loginPage = "/login.jsp", errorPage = "/login.jsp") ) @Path("authorize") public class AuthorizationEndpoint { //... @GET @Produces(MediaType.TEXT_HTML) public Response doGet(@Context HttpServletRequest request, @Context HttpServletResponse response, @Context UriInfo uriInfo) throws ServletException, IOException { MultivaluedMap params = uriInfo.getQueryParameters(); Principal principal = securityContext.getCallerPrincipal(); // ... } }

At this point, the authorization endpoint can start processing the application's request, which must contain response_type and client_id parameters and – optionally, but recommended – the redirect_uri, scope, and state parameters.

The client_id should be a valid client, in our case from the clients database table.

The redirect_uri, if specified, should also match what we find in the clients database table.

And, because we're doing Authorization Code, response_type is code.

Since authorization is a multi-step process, we can temporarily store these values in the session:

request.getSession().setAttribute("ORIGINAL_PARAMS", params);

And then prepare to ask the user which permissions the application may use, redirecting to that page:

String allowedScopes = checkUserScopes(user.getScopes(), requestedScope); request.setAttribute("scopes", allowedScopes); request.getRequestDispatcher("/authorize.jsp").forward(request, response);

3.3. User Scopes Approval

At this point, the browser renders an authorization UI for the user, and the user makes a selection. Then, the browser submits the user's selection in an HTTP POST:

@POST @Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Produces(MediaType.TEXT_HTML) public Response doPost(@Context HttpServletRequest request, @Context HttpServletResponse response, MultivaluedMap params) throws Exception { MultivaluedMap originalParams = (MultivaluedMap) request.getSession().getAttribute("ORIGINAL_PARAMS"); // ... String approvalStatus = params.getFirst("approval_status"); // YES OR NO // ... if YES List approvedScopes = params.get("scope"); // ... }

Next, we generate a temporary code that refers to the user_id, client_id, andredirect_uri, all of which the application will use later when it hits the token endpoint.

So let's create an AuthorizationCode JPA Entity with an auto-generated id:

@Entity @Table(name) public class AuthorizationCode { @Id @GeneratedValue(strategy=GenerationType.AUTO) @Column(name = "code") private String code; //... }

And then populate it:

AuthorizationCode authorizationCode = new AuthorizationCode(); authorizationCode.setClientId(clientId); authorizationCode.setUserId(userId); authorizationCode.setApprovedScopes(String.join(" ", authorizedScopes)); authorizationCode.setExpirationDate(LocalDateTime.now().plusMinutes(2)); authorizationCode.setRedirectUri(redirectUri);

When we save the bean, the code attribute is auto-populated, and so we can get it and send it back to the client:

appDataRepository.save(authorizationCode); String code = authorizationCode.getCode();

Note that our authorization code will expire in two minutes – we should be as conservative as we can with this expiration. It can be short since the client is going to exchange it right away for an access token.

We then redirect back to the application's redirect_uri, giving it the code as well as any state parameter that the application specified in its /authorize request:

StringBuilder sb = new StringBuilder(redirectUri); // ... sb.append("?code=").append(code); String state = params.getFirst("state"); if (state != null) { sb.append("&state=").append(state); } URI location = UriBuilder.fromUri(sb.toString()).build(); return Response.seeOther(location).build();

Note again that redirectUri is whatever exists in the clients table, not the redirect_uri request parameter.

So, our next step is for the client to receive this code and exchange it for an access token using the token endpoint.

3.4. Token Endpoint

As opposed to the authorization endpoint, the token endpoint doesn't need a browser to communicate with the client, and we'll, therefore, implement it as a JAX-RS endpoint:

@Path("token") public class TokenEndpoint { List supportedGrantTypes = Collections.singletonList("authorization_code"); @Inject private AppDataRepository appDataRepository; @Inject Instance authorizationGrantTypeHandlers; @POST @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_FORM_URLENCODED) public Response token(MultivaluedMap params, @HeaderParam(HttpHeaders.AUTHORIZATION) String authHeader) throws JOSEException { //... } }

The token endpoint requires a POST, as well as encoding the parameters using the application/x-www-form-urlencoded media type.

As we discussed, we'll be supporting only the authorization code grant type:

List supportedGrantTypes = Collections.singletonList("authorization_code");

So, the received grant_type as a required parameter should be supported:

String grantType = params.getFirst("grant_type"); Objects.requireNonNull(grantType, "grant_type params is required"); if (!supportedGrantTypes.contains(grantType)) { JsonObject error = Json.createObjectBuilder() .add("error", "unsupported_grant_type") .add("error_description", "grant type should be one of :" + supportedGrantTypes) .build(); return Response.status(Response.Status.BAD_REQUEST) .entity(error).build(); }

Next, we check the client authentication through via HTTP Basic authentication. That is, we check if the received client_id and client_secret, through the Authorization header, matches a registered client:

String[] clientCredentials = extract(authHeader); String clientId = clientCredentials[0]; String clientSecret = clientCredentials[1]; Client client = appDataRepository.getClient(clientId); if (client == null || clientSecret == null || !clientSecret.equals(client.getClientSecret())) { JsonObject error = Json.createObjectBuilder() .add("error", "invalid_client") .build(); return Response.status(Response.Status.UNAUTHORIZED) .entity(error).build(); }

Finally, we delegate the production of the TokenResponse to a corresponding grant type handler:

public interface AuthorizationGrantTypeHandler { TokenResponse createAccessToken(String clientId, MultivaluedMap params) throws Exception; }

As we're more interested in the authorization code grant type, we've provided an adequate implementation as a CDI bean and decorated it with the Named annotation:

@Named("authorization_code")

At runtime, and according to the received grant_type value, the corresponding implementation is activated through the CDI Instance mechanism:

String grantType = params.getFirst("grant_type"); //... AuthorizationGrantTypeHandler authorizationGrantTypeHandler = authorizationGrantTypeHandlers.select(NamedLiteral.of(grantType)).get();

It's now time to produce /token‘s response.

3.5. RSA Private and Public Keys

Before generating the token, we need an RSA private key for signing tokens.

For this purpose, we'll be using OpenSSL:

# PRIVATE KEY openssl genpkey -algorithm RSA -out private-key.pem -pkeyopt rsa_keygen_bits:2048

The private-key.pem is provided to the server through the MicroProfile Config signingKey property using the file META-INF/microprofile-config.properties:

signingkey=/META-INF/private-key.pem

The server can read the property using the injected Config object:

String signingkey = config.getValue("signingkey", String.class);

Similarly, we can generate the corresponding public key:

# PUBLIC KEY openssl rsa -pubout -in private-key.pem -out public-key.pem

And use the MicroProfile Config verificationKey to read it:

verificationkey=/META-INF/public-key.pem

The server should make it available for the resource server for the purpose of verification. This is done through a JWK endpoint.

Nimbus JOSE+JWT is a library that can be a big help here. Let's first add the nimbus-jose-jwt dependency:

 com.nimbusds nimbus-jose-jwt 7.7 

And now, we can leverage Nimbus's JWK support to simplify our endpoint:

@Path("jwk") @ApplicationScoped public class JWKEndpoint { @GET public Response getKey(@QueryParam("format") String format) throws Exception { //... String verificationkey = config.getValue("verificationkey", String.class); String pemEncodedRSAPublicKey = PEMKeyUtils.readKeyAsString(verificationkey); if (format == null || format.equals("jwk")) { JWK jwk = JWK.parseFromPEMEncodedObjects(pemEncodedRSAPublicKey); return Response.ok(jwk.toJSONString()).type(MediaType.APPLICATION_JSON).build(); } else if (format.equals("pem")) { return Response.ok(pemEncodedRSAPublicKey).build(); } //... } }

We've used the format parameter to switch between the PEM and JWK formats. The MicroProfile JWT which we'll use for implementing the resource server supports both these formats.

3.6. Token Endpoint Response

It's now time for a given AuthorizationGrantTypeHandler to create the token response. In this implementation, we'll support only the structured JWT Tokens.

For creating a token in this format, we'll again use the Nimbus JOSE+JWT library, but there are numerous other JWT libraries, too.

So, to create a signed JWT, we first have to construct the JWT header:

JWSHeader jwsHeader = new JWSHeader.Builder(JWSAlgorithm.RS256).type(JOSEObjectType.JWT).build();

Then, we build the payload which is a Set of standardized and custom claims:

Instant now = Instant.now(); Long expiresInMin = 30L; Date in30Min = Date.from(now.plus(expiresInMin, ChronoUnit.MINUTES)); JWTClaimsSet jwtClaims = new JWTClaimsSet.Builder() .issuer("//localhost:9080") .subject(authorizationCode.getUserId()) .claim("upn", authorizationCode.getUserId()) .audience("//localhost:9280") .claim("scope", authorizationCode.getApprovedScopes()) .claim("groups", Arrays.asList(authorizationCode.getApprovedScopes().split(" "))) .expirationTime(in30Min) .notBeforeTime(Date.from(now)) .issueTime(Date.from(now)) .jwtID(UUID.randomUUID().toString()) .build(); SignedJWT signedJWT = new SignedJWT(jwsHeader, jwtClaims);

In addition to the standard JWT claims, we've added two more claims – upn and groups – as they're needed by the MicroProfile JWT. The upn will be mapped to the Jakarta EE Security CallerPrincipal and the groups will be mapped to Jakarta EE Roles.

Now that we have the header and the payload, we need to sign the access token with an RSA private key. The corresponding RSA public key will be exposed through the JWK endpoint or made available by other means so that the resource server can use it to verify the access token.

As we've provided the private key as a PEM format, we should retrieve it and transform it into an RSAPrivateKey:

SignedJWT signedJWT = new SignedJWT(jwsHeader, jwtClaims); //... String signingkey = config.getValue("signingkey", String.class); String pemEncodedRSAPrivateKey = PEMKeyUtils.readKeyAsString(signingkey); RSAKey rsaKey = (RSAKey) JWK.parseFromPEMEncodedObjects(pemEncodedRSAPrivateKey);

Next, we sign and serialize the JWT:

signedJWT.sign(new RSASSASigner(rsaKey.toRSAPrivateKey())); String accessToken = signedJWT.serialize();

And finally we construct a token response:

return Json.createObjectBuilder() .add("token_type", "Bearer") .add("access_token", accessToken) .add("expires_in", expiresInMin * 60) .add("scope", authorizationCode.getApprovedScopes()) .build();

which is, thanks to JSON-P, serialized to JSON format and sent to the client:

{ "access_token": "acb6803a48114d9fb4761e403c17f812", "token_type": "Bearer", "expires_in": 1800, "scope": "resource.read resource.write" }

4. OAuth 2.0 Client

In this section, we'll be building a web-based OAuth 2.0 Client using the Servlet, MicroProfile Config, and JAX RS Client APIs.

More precisely, we'll be implementing two main servlets: one for requesting the authorization server's authorization endpoint and getting a code using the authorization code grant type, and another servlet for using the received code and requesting an access token from the authorization server's token endpoint.

Additionally, we'll be implementing two more servlets: One for getting a new access token using the refresh token grant type, and another for accessing the resource server's APIs.

4.1. OAuth 2.0 Client Details

As the client is already registered within the authorization server, we first need to provide the client registration information:

  • client_id: Client Identifier and it's usually issued by the authorization server during the registration process.
  • client_secret: Client Secret.
  • redirect_uri: Location where to receive the authorization code.
  • scope: Client requested permissions.

Additionally, the client should know the authorization server's authorization and token endpoints:

  • authorization_uri: Location of the authorization server authorization endpoint that we can use to get a code.
  • token_uri: Location of the authorization server token endpoint that we can use to get a token.

All this information is provided through the MicroProfile Config file, META-INF/microprofile-config.properties:

# Client registration client.clientId=webappclient client.clientSecret=webappclientsecret client.redirectUri=//localhost:9180/callback client.scope=resource.read resource.write # Provider provider.authorizationUri=//127.0.0.1:9080/authorize provider.tokenUri=//127.0.0.1:9080/token

4.2. Authorization Code Request

The flow of getting an authorization code starts with the client by redirecting the browser to the authorization server's authorization endpoint.

Typically, this happens when the user tries to access a protected resource API without authorization, or by explicitly by invoking the client /authorize path:

@WebServlet(urlPatterns = "/authorize") public class AuthorizationCodeServlet extends HttpServlet { @Inject private Config config; @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { //... } }

In the doGet() method, we start by generating and storing a security state value:

String state = UUID.randomUUID().toString(); request.getSession().setAttribute("CLIENT_LOCAL_STATE", state);

Then, we retrieve the client configuration information:

String authorizationUri = config.getValue("provider.authorizationUri", String.class); String clientId = config.getValue("client.clientId", String.class); String redirectUri = config.getValue("client.redirectUri", String.class); String scope = config.getValue("client.scope", String.class);

We'll then append these pieces of information as query parameters to the authorization server's authorization endpoint:

String authorizationLocation = authorizationUri + "?response_type=code" + "&client_id=" + clientId + "&redirect_uri=" + redirectUri + "&scope=" + scope + "&state=" + state;

And finally, we'll redirect the browser to this URL:

response.sendRedirect(authorizationLocation);

After processing the request, the authorization server's authorization endpoint will generate and append a code, in addition to the received state parameter, to the redirect_uri and will redirect back the browser //localhost:9081/callback?code=A123&state=Y.

4.3. Access Token Request

The client callback servlet, /callback, begins by validating the received state:

String localState = (String) request.getSession().getAttribute("CLIENT_LOCAL_STATE"); if (!localState.equals(request.getParameter("state"))) { request.setAttribute("error", "The state attribute doesn't match!"); dispatch("/", request, response); return; }

Next, we'll use the code we previously received to request an access token through the authorization server's token endpoint:

String code = request.getParameter("code"); Client client = ClientBuilder.newClient(); WebTarget target = client.target(config.getValue("provider.tokenUri", String.class)); Form form = new Form(); form.param("grant_type", "authorization_code"); form.param("code", code); form.param("redirect_uri", config.getValue("client.redirectUri", String.class)); TokenResponse tokenResponse = target.request(MediaType.APPLICATION_JSON_TYPE) .header(HttpHeaders.AUTHORIZATION, getAuthorizationHeaderValue()) .post(Entity.entity(form, MediaType.APPLICATION_FORM_URLENCODED_TYPE), TokenResponse.class);

As we can see, there's no browser interaction for this call, and the request is made directly using the JAX-RS client API as an HTTP POST.

As the token endpoint requires the client authentication, we have included the client credentials client_id and client_secret in the Authorization header.

The client can use this access token to invoke the resource server APIs which is the subject of the next subsection.

4.4. Protected Resource Access

At this point, we have a valid access token and we can call the resource server's /read and /write APIs.

To do that, we have to provide the Authorization header. Using the JAX-RS Client API, this is simply done through the Invocation.Builder header() method:

resourceWebTarget = webTarget.path("resource/read"); Invocation.Builder invocationBuilder = resourceWebTarget.request(); response = invocationBuilder .header("authorization", tokenResponse.getString("access_token")) .get(String.class);

5. OAuth 2.0 Resource Server

In this section, we'll be building a secured web application based on JAX-RS, MicroProfile JWT, and MicroProfile Config. The MicroProfile JWT takes care of validating the received JWT and mapping the JWT scopes to Jakarta EE roles.

5.1. Maven Dependencies

In addition to the Java EE Web API dependency, we need also the MicroProfile Config and MicroProfile JWT APIs:

 javax javaee-web-api 8.0 provided   org.eclipse.microprofile.config microprofile-config-api 1.3   org.eclipse.microprofile.jwt microprofile-jwt-auth-api 1.1 

5.2. JWT Authentication Mechanism

The MicroProfile JWT provides an implementation of the Bearer Token Authentication mechanism. This takes care of processing the JWT present in the Authorization header, makes available a Jakarta EE Security Principal as a JsonWebToken which holds the JWT claims, and maps the scopes to Jakarta EE roles. Take a look at the Jakarta EE Security API for more background.

To enable the JWT authentication mechanism in the server, we need to add the LoginConfig annotation in the JAX-RS application:

@ApplicationPath("/api") @DeclareRoles({"resource.read", "resource.write"}) @LoginConfig(authMethod = "MP-JWT") public class OAuth2ResourceServerApplication extends Application { }

Additionally, MicroProfile JWT needs the RSA public key in order to verify the JWT signature. We can provide this either by introspection or, for simplicity, by manually copying the key from the authorization server. In either case, we need to provide the location of the public key:

mp.jwt.verify.publickey.location=/META-INF/public-key.pem

Finally, the MicroProfile JWT needs to verify the iss claim of the incoming JWT, which should be present and match the value of the MicroProfile Config property:

mp.jwt.verify.issuer=//127.0.0.1:9080

Typically, this is the location of the Authorization Server.

5.3. The Secured Endpoints

For demonstration purposes, we'll add a resource API with two endpoints. One is a read endpoint that's accessible by users having the resource.read scope and another write endpoint for users with resource.write scope.

The restriction on the scopes is done through the @RolesAllowed annotation:

@Path("/resource") @RequestScoped public class ProtectedResource { @Inject private JsonWebToken principal; @GET @RolesAllowed("resource.read") @Path("/read") public String read() { return "Protected Resource accessed by : " + principal.getName(); } @POST @RolesAllowed("resource.write") @Path("/write") public String write() { return "Protected Resource accessed by : " + principal.getName(); } }

6. Running All Servers

To run one server, we just need to invoke the Maven command in the corresponding directory:

mvn package liberty:run-server

The authorization server, the client and the resource server will be running and available respectively at the following locations:

# Authorization Server //localhost:9080/ # Client //localhost:9180/ # Resource Server //localhost:9280/ 

So, we can access the client home page and then we click on “Get Access Token” to start the authorization flow. After receiving the access token, we can access the resource server's read and write APIs.

Depending on the granted scopes, the resource server will respond either by a successful message or we'll get an HTTP 403 forbidden status.

7. Conclusion

In this article, we've provided an implementation of an OAuth 2.0 Authorization Server that can be used with any compatible OAuth 2.0 Client and Resource Server.

Um das allgemeine Framework zu erläutern, haben wir auch eine Implementierung für den Client und den Ressourcenserver bereitgestellt. Um all diese Komponenten zu implementieren, haben wir Jakarta EE 8-APIs verwendet, insbesondere CDI, Servlet, JAX RS und Jakarta EE Security. Zusätzlich haben wir die Pseudo-Jakarta-EE-APIs des MicroProfile verwendet: MicroProfile Config und MicroProfile JWT.

Der vollständige Quellcode für die Beispiele ist auf GitHub verfügbar. Beachten Sie, dass der Code ein Beispiel sowohl für den Berechtigungscode als auch für die Erteilungstypen für Aktualisierungstoken enthält.

Schließlich ist es wichtig, sich des pädagogischen Charakters dieses Artikels bewusst zu sein und dass das angegebene Beispiel nicht in Produktionssystemen verwendet werden sollte.