Implementatie van het OAuth 2.0-autorisatiekader met behulp van Jakarta EE

1. Overzicht

In deze tutorial gaan we een implementatie bieden voor het OAuth 2.0 Authorization Framework met behulp van Jakarta EE en MicroProfile. Het belangrijkste is dat we de interactie van de OAuth 2.0-rollen gaan implementeren via het toekenningstype Autorisatiecode. De motivatie achter dit schrijven is om ondersteuning te bieden voor projecten die worden geïmplementeerd met Jakarta EE, aangezien dit nog geen ondersteuning biedt voor OAuth.

Voor de belangrijkste rol, de autorisatieserver, we gaan het autorisatie-eindpunt, het token-eindpunt en bovendien het JWK-sleutel-eindpunt implementeren, wat handig is voor de Resource Server om de openbare sleutel op te halen.

Omdat we willen dat de implementatie eenvoudig en gemakkelijk is voor een snelle installatie, gaan we een vooraf geregistreerde winkel van klanten en gebruikers gebruiken, en uiteraard een JWT-winkel voor toegangstokens.

Voordat u direct op het onderwerp ingaat, is het belangrijk op te merken dat het voorbeeld in deze tutorial voor educatieve doeleinden is. Voor productiesystemen wordt het ten zeerste aanbevolen om een ​​volwassen, goed geteste oplossing zoals Keycloak te gebruiken.

2. OAuth 2.0 Overzicht

In deze sectie geven we een kort overzicht van de OAuth 2.0-rollen en de autorisatiecode-toekenningsstroom.

2.1. Rollen

Het OAuth 2.0-framework impliceert de samenwerking tussen de volgende vier rollen:

  • Eigenaar van de bron: Meestal is dit de eindgebruiker - het is de entiteit die een aantal bronnen heeft die het waard zijn om te beschermen
  • Bronserver: Een service die de gegevens van de resource-eigenaar beschermt, meestal door deze te publiceren via een REST API
  • Cliënt: Een toepassing die de gegevens van de resource-eigenaar gebruikt
  • Autorisatieserver: Een applicatie die toestemming - of autoriteit - verleent aan klanten in de vorm van verlopen tokens

2.2. Soorten autorisatiesubsidies

EEN toekenningstype is hoe een klant toestemming krijgt om de gegevens van de resource-eigenaar te gebruiken, uiteindelijk in de vorm van een toegangstoken.

Uiteraard geven verschillende soorten klanten de voorkeur aan verschillende soorten subsidies:

  • Authorisatie Code: Meest geprefereerdof het is een webtoepassing, een systeemeigen toepassing of een toepassing met één pagina, hoewel native apps en apps met één pagina extra bescherming nodig hebben, genaamd PKCE
  • Vernieuwingstoken: Een speciale verlengingssubsidie, geschikt voor webapplicaties om hun bestaande token te vernieuwen
  • Klantreferenties: Voorkeur voor service-to-service-communicatie, bijvoorbeeld wanneer de eigenaar van de bron geen eindgebruiker is
  • Eigenaar van de bronWachtwoord: Voorkeur voor de first-party authenticatie van native applicaties, zeg wanneer de mobiele app een eigen inlogpagina nodig heeft

Bovendien kan de klant de impliciet toekenningstype. Het is echter meestal veiliger om de autorisatiecode-toekenning te gebruiken met PKCE.

2.3. Autorisatiecode toekenningsstroom

Aangezien de autorisatiecode-verleningsstroom de meest voorkomende is, laten we ook kijken hoe dat werkt, en dat is eigenlijk wat we in deze tutorial zullen bouwen.

Een applicatie - een klant - vraagt ​​toestemming door om te leiden naar de autorisatieserver /toestemming geven eindpunt. Aan dit eindpunt geeft de applicatie een Bel terug eindpunt.

De autorisatieserver vraagt ​​meestal de eindgebruiker - de resource-eigenaar - om toestemming. Als de eindgebruiker toestemming geeft, dan de autorisatieserver verwijst terug naar de callback met een code.

De applicatie ontvangt deze code en vervolgens voert een geauthenticeerde oproep uit naar de autorisatieserver / token eindpunt. Met "geauthenticeerd" bedoelen we dat de applicatie bewijst wie het is als onderdeel van deze oproep. Als alles in de juiste volgorde wordt weergegeven, antwoordt de autorisatieserver met het token.

Met het teken in de hand, de applicatie doet zijn verzoek aan de API - de bronserver - en die API zal het token verifiëren. Het kan de autorisatieserver vragen om het token te verifiëren met behulp van zijn /doordringen eindpunt. Of, als het token op zichzelf staand is, kan de bronserver optimaliseren door lokaal verifiëren van de handtekening van het token, zoals het geval is bij JWT.

2.4. Wat ondersteunt Jakarta EE?

Nog niet veel. In deze tutorial bouwen we de meeste dingen vanaf de grond af.

3. OAuth 2.0-autorisatieserver

In deze implementatie zullen we ons concentreren op het meest gebruikte subsidietype: Authorisatie Code.

3.1. Klant- en gebruikersregistratie

Een autorisatieserver moet natuurlijk op de hoogte zijn van de klanten en gebruikers voordat deze hun verzoeken kan autoriseren. En het is gebruikelijk dat een autorisatieserver hiervoor een gebruikersinterface heeft.

Voor de eenvoud gebruiken we echter een vooraf geconfigureerde client:

INVOEGEN IN clients (client_id, client_secret, redirect_uri, scope, geautoriseerde_grant_types) VALUES ('webappclient', 'webappclientsecret', '// localhost: 9180 / callback', 'resource.read resource.write', 'autorisatiecode refresh_token');
@Entity @Table (name = "clients") publieke klasse 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; // ...}

En een vooraf geconfigureerde gebruiker:

INVOEGEN IN gebruikers (user_id, wachtwoord, rollen, scopes) WAARDEN ('appuser', 'appusersecret', 'USER', 'resource.read resource.write');
@Entity @Table (naam = "gebruikers") openbare klasse Gebruiker implementeert Principal {@Id @Column (naam = "user_id") privé String userId; @Column (name = "wachtwoord") privé String-wachtwoord; @Column (name = "rollen") privé String-rollen; @Column (name = "scopes") private String-scopes; // ...}

Merk op dat we omwille van deze tutorial wachtwoorden in platte tekst hebben gebruikt, maar in een productieomgeving moeten ze worden gehasht.

Voor de rest van deze tutorial laten we zien hoe appgebruiker - de resource-eigenaar - kan toegang verlenen tot webappclient - de toepassing - door de autorisatiecode te implementeren.

3.2. Autorisatie-eindpunt

De belangrijkste rol van het autorisatie-eindpunt is eerst authenticeer de gebruiker en vraag vervolgens om de rechten - of scopes - die de applicatie wil.

Zoals aangegeven door de OAuth2-specificaties, zou dit eindpunt de HTTP GET-methode moeten ondersteunen, hoewel het ook de HTTP POST-methode kan ondersteunen. In deze implementatie ondersteunen we alleen de HTTP GET-methode.

Eerste, het autorisatie-eindpunt vereist dat de gebruiker wordt geverifieerd. De specificatie vereist hier geen bepaalde manier, dus laten we Form Authentication gebruiken van de Jakarta EE 8 Security API:

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

De gebruiker wordt omgeleid naar /login.jsp voor authenticatie en zal dan beschikbaar zijn als een CallerPrincipal via de SecurityContext API:

Principal principal = securityContext.getCallerPrincipal ();

We kunnen deze samenstellen met JAX-RS:

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

Op dit punt kan het autorisatie-eindpunt beginnen met het verwerken van het verzoek van de toepassing, dat response_type en klant identificatie parameters en - optioneel, maar aanbevolen - het redirect_uri, bereik, en staat parameters.

De klant identificatie moet een geldige klant zijn, in ons geval van de klanten databasetabel.

De redirect_uri, indien gespecificeerd, moet ook overeenkomen met wat we vinden in het klanten databasetabel.

En omdat we autorisatiecode aan het doen zijn, response_type is code.

Omdat autorisatie een proces met meerdere stappen is, kunnen we deze waarden tijdelijk in de sessie opslaan:

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

En bereid je dan voor om de gebruiker te vragen welke machtigingen de applicatie mag gebruiken, door om te leiden naar die pagina:

String allowScopes = checkUserScopes (user.getScopes (), requestScope); request.setAttribute ("scopes", allowScopes); request.getRequestDispatcher ("/ authorize.jsp"). doorsturen (verzoek, antwoord);

3.3. Goedkeuring gebruikersbereik

Op dit punt geeft de browser een autorisatie-gebruikersinterface weer voor de gebruiker, en de gebruiker maakt een keuze. Vervolgens de browser dient de gebruikersselectie in in een HTTP POST:

@POST @Consumes (MediaType.APPLICATION_FORM_URLENCODED) @Produces (MediaType.TEXT_HTML) openbare reactie doPost (@Context HttpServletRequest-verzoek, @Context HttpServletResponse-antwoord, MultivaluedMap-params) gooit Uitzondering {MultivaluedMap) -verzoek. "ORIGINAL_PARAMS"); // ... String goedkeuringsstatus = params.getFirst ("goedkeuringsstatus"); // JA OF NEE // ... indien JA Lijst goedgekeurdScopes = params.get ("scope"); // ...}

Vervolgens genereren we een tijdelijke code die verwijst naar de user_id, client_id, enredirect_uri, die de applicatie later allemaal zal gebruiken wanneer het het token-eindpunt raakt.

Dus laten we een Authorisatie Code JPA-entiteit met een automatisch gegenereerde ID:

@Entity @Table (naam) openbare klasse AuthorizationCode {@Id @GeneratedValue (strategie = GenerationType.AUTO) @Column (naam = "code") private String-code; // ...}

En vul het dan in:

AuthorizationCode authorizationCode = nieuwe AuthorizationCode (); autorisatieCode.setClientId (clientId); autorisatieCode.setUserId (gebruikers-ID); autorisatieCode.setApprovedScopes (String.join ("", AuthorizedScopes)); autorisatieCode.setExpirationDate (LocalDateTime.now (). plusMinutes (2)); autorisatieCode.setRedirectUri (redirectUri);

Wanneer we de bean opslaan, wordt het codekenmerk automatisch ingevuld, zodat we het kunnen ophalen en terugsturen naar de klant:

appDataRepository.save (autorisatiecode); Tekenreekscode = autorisatieCode.getCode ();

Let daar op onze autorisatiecode verloopt over twee minuten - we moeten zo conservatief mogelijk zijn met deze afloop. Het kan kort zijn omdat de klant het meteen gaat inwisselen voor een toegangstoken.

We verwijzen vervolgens terug naar het redirect_uri, door het de code te geven, evenals alle andere staat parameter die de toepassing heeft opgegeven in zijn /toestemming geven verzoek:

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

Merk nogmaals op dat redirectUri is wat er in de klanten tafel, niet de redirect_uri verzoekparameter.

Onze volgende stap is dus dat de klant deze code ontvangt en deze inwisselt voor een toegangstoken met behulp van het token-eindpunt.

3.4. Token-eindpunt

In tegenstelling tot het autorisatie-eindpunt, het token-eindpunt heeft geen browser nodig om met de klant te communiceren, en we zullen het daarom implementeren als een JAX-RS-eindpunt:

@Path ("token") openbare klasse TokenEndpoint {List ondersteundeGrantTypes = Collections.singletonList ("autorisatiecode"); @Inject privé AppDataRepository appDataRepository; @Inject Instance autorisatieGrantTypeHandlers; @POST @Produces (MediaType.APPLICATION_JSON) @Consumes (MediaType.APPLICATION_FORM_URLENCODED) openbaar antwoordtoken (MultivaluedMap-params, @HeaderParam (HttpHeaders.AUTHORIZATION) String authHeader) gooit JOSEException {// ...}}

Het token-eindpunt vereist een POST, evenals het coderen van de parameters met behulp van de application / x-www-form-urlencoded mediatype.

Zoals we hebben besproken, ondersteunen we alleen het Authorisatie Code subsidietype:

Lijst ondersteundGrantTypes = Collections.singletonList ("autorisatiecode");

Dus de ontvangen grant_type aangezien een vereiste parameter moet worden ondersteund:

String grantType = params.getFirst ("grant_type"); Objects.requireNonNull (grantType, "grant_type params is vereist"); if (! SupportedGrantTypes.contains (grantType)) {JsonObject error = Json.createObjectBuilder () .add ("error", "unsupported_grant_type") .add ("error_description", "toekenningstype moet een van de volgende zijn:" + ondersteundeGrantTypes). bouwen(); return Response.status (Response.Status.BAD_REQUEST) .entity (fout) .build (); }

Vervolgens controleren we de client-authenticatie via HTTP Basic-authenticatie. Dat wil zeggen, we controleren als het ontvangen klant identificatie en client_secret, door het Autorisatie koptekst, komt overeen met een geregistreerde klant:

String [] clientCredentials = extract (autheader); 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 (fout) .build (); }

Ten slotte delegeren we de productie van de TokenResponse naar een overeenkomstige handler van het toekenningstype:

openbare interface AuthorizationGrantTypeHandler {TokenResponse createAccessToken (String clientId, MultivaluedMap-params) genereert Uitzondering; }

Omdat we meer geïnteresseerd zijn in het toekenningstype autorisatiecode, hebben we een adequate implementatie als een CDI-bean geleverd en deze versierd met de Genaamd annotatie:

@Named ("autorisatiecode")

Tijdens runtime, en volgens het ontvangen grant_type waarde, wordt de bijbehorende implementatie geactiveerd via het CDI Instance-mechanisme:

String grantType = params.getFirst ("grant_type"); // ... AuthorizationGrantTypeHandler autorisatieGrantTypeHandler = autorisatieGrantTypeHandlers.select (NamedLiteral.of (grantType)). Get ();

Het is nu tijd om te produceren / token‘S reactie.

3.5. RSA Privé- en openbare sleutels

Voordat we het token genereren, hebben we een persoonlijke RSA-sleutel nodig voor het ondertekenen van tokens.

Voor dit doel gebruiken we OpenSSL:

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

De private-key.pem wordt aan de server geleverd via de MicroProfile Config SigningKey eigenschap met behulp van het bestand META-INF / microprofile-config.properties:

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

De server kan de eigenschap lezen met behulp van het geïnjecteerde Config voorwerp:

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

Evenzo kunnen we de bijbehorende openbare sleutel genereren:

# OPENBARE SLEUTEL openssl rsa -pubout -in private-key.pem -out public-key.pem

En gebruik de MicroProfile Config verificatieKey om het te lezen:

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

De server moet het beschikbaar maken voor de bronserver voor het doel van verificatie. Dit is gedaan via een JWK-eindpunt.

Nimbus JOSE + JWT is een bibliotheek die hier een grote hulp kan zijn. Laten we eerst de nimbus-jose-jwt afhankelijkheid:

 com.nimbusds nimbus-jose-jwt 7.7 

En nu kunnen we gebruikmaken van de JWK-ondersteuning van Nimbus om ons eindpunt te vereenvoudigen:

@Path ("jwk") @ApplicationScoped openbare klasse JWKEndpoint {@GET openbare respons getKey (@QueryParam ("format") String-indeling) genereert Uitzondering {// ... String verificatiekey = config.getValue ("verificatiecode", String. klasse); String pemEncodedRSAPublicKey = PEMKeyUtils.readKeyAsString (verificatiesleutel); 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 hebben het formaat gebruikt parameter om te schakelen tussen de PEM- en JWK-formaten. De MicroProfile JWT die we zullen gebruiken voor het implementeren van de bronserver ondersteunt beide formaten.

3.6. Token Endpoint Response

Het is nu tijd voor een gegeven AutorisatieGrantTypeHandler om het tokenantwoord te creëren. In deze implementatie ondersteunen we alleen de gestructureerde JWT-tokens.

Om een ​​token in deze indeling te maken, gebruiken we opnieuw de Nimbus JOSE + JWT-bibliotheek, maar er zijn ook tal van andere JWT-bibliotheken.

Dus om een ​​ondertekende JWT te maken, we moeten eerst de JWT-header construeren:

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

Vervolgens bouwen we de payload wat een is Set van gestandaardiseerde claims en claims op maat:

Instant now = Instant.now (); Long expiresInMin = 30L; Date in30Min = Date.from (now.plus (expiresInMin, ChronoUnit.MINUTES)); JWTClaimsSet jwtClaims = nieuwe JWTClaimsSet.Builder () .issuer ("// localhost: 9080") .subject (authorisatieCode.getUserId ()) .claim ("upn", autorisatieCode.getUserId ()) .audience ("// localhost: 9280 ") .claim (" scope ", autorisatieCode.getApprovedScopes ()) .claim (" groups ", Arrays.asList (authorisatieCode.getApprovedScopes (). Split (" "))) .expirationTime (in30Min) .notBeforeTime (Date. van (nu)) .issueTime (Datum.vanaf (nu)) .jwtID (UUID.randomUUID (). toString ()) .build (); SignedJWT getekendJWT = nieuw SignedJWT (jwsHeader, jwtClaims);

Naast de standaard JWT-claims hebben we nog twee claims toegevoegd - upn en groepen - zoals ze nodig zijn door de MicroProfile JWT. De upn zal worden toegewezen aan de Jakarta EE Security CallerPrincipal en de groepen wordt toegewezen aan Jakarta EE Rollen.

Nu we de header en de payload hebben, we moeten het toegangstoken ondertekenen met een persoonlijke RSA-sleutel. De corresponderende openbare RSA-sleutel wordt weergegeven via het JWK-eindpunt of wordt op een andere manier beschikbaar gesteld, zodat de bronserver deze kan gebruiken om het toegangstoken te verifiëren.

Aangezien we de privésleutel als een PEM-indeling hebben verstrekt, moeten we deze ophalen en omzetten in een RSAPrivateKey:

SignedJWT getekendJWT = nieuw SignedJWT (jwsHeader, jwtClaims); // ... String Signingkey = config.getValue ("Signingkey", String.class); String pemEncodedRSAPrivateKey = PEMKeyUtils.readKeyAsString (signeersleutel); RSAKey rsaKey = (RSAKey) JWK.parseFromPEMEncodedObjects (pemEncodedRSAPrivateKey);

De volgende, we ondertekenen en serialiseren de JWT:

ondertekendJWT.sign (nieuwe RSASSASigner (rsaKey.toRSAPrivateKey ())); String accessToken = ondertekendJWT.serialize ();

En tenslotte we construeren een tokenantwoord:

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

dat is, dankzij JSON-P, geserialiseerd naar JSON-formaat en verzonden naar de klant:

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

4. OAuth 2.0-client

In deze sectie zullen we zijn het bouwen van een webgebaseerde OAuth 2.0-client met behulp van de Servlet, MicroProfile Config en JAX RS Client API's.

Om precies te zijn, we zullen twee hoofdservlets implementeren: een voor het aanvragen van het autorisatie-eindpunt van de autorisatieserver en het verkrijgen van een code met behulp van het autorisatiecode-toekenningstype, en een andere servlet voor het gebruiken van de ontvangen code en het aanvragen van een toegangstoken van het tokeneindpunt van de autorisatieserver. .

Daarnaast zullen we nog twee servlets implementeren: een voor het verkrijgen van een nieuw toegangstoken met het toekenningstype vernieuwingstoken en een andere voor toegang tot de API's van de bronserver.

4.1. OAuth 2.0-clientgegevens

Aangezien de client al is geregistreerd op de autorisatieserver, moeten we eerst de clientregistratiegegevens verstrekken:

  • klant identificatie: Client-ID en wordt meestal uitgegeven door de autorisatieserver tijdens het registratieproces.
  • client_secret: Cliëntgeheim.
  • redirect_uri: Locatie waar u de autorisatiecode kunt ontvangen.
  • reikwijdte: De klant heeft toestemming gevraagd.

Bovendien moet de client de autorisatie- en tokeneindpunten van de autorisatieserver kennen:

  • autorisatie_uri: Locatie van het autorisatie-eindpunt van de autorisatieserver dat we kunnen gebruiken om een ​​code op te halen.
  • token_uri: Locatie van het autorisatieserver-tokeneindpunt dat we kunnen gebruiken om een ​​token op te halen.

Al deze informatie wordt verstrekt via het MicroProfile Config-bestand, META-INF / microprofile-config.properties:

# Clientregistratie 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. Verzoek om autorisatiecode

Het verkrijgen van een autorisatiecode begint bij de client door de browser om te leiden naar het autorisatie-eindpunt van de autorisatieserver.

Dit gebeurt meestal wanneer de gebruiker zonder autorisatie toegang probeert te krijgen tot een beschermde bron-API, of door expliciet de client aan te roepen /toestemming geven pad:

@WebServlet (urlPatterns = "/ authorize") openbare klasse AuthorizationCodeServlet breidt HttpServlet {@Inject private Config-configuratie uit; @Override beschermde ongeldige doGet (HttpServletRequest-verzoek, HttpServletResponse-reactie) gooit ServletException, IOException {// ...}}

In de doGet () methode, beginnen we met het genereren en opslaan van een beveiligingsstatuswaarde:

Tekenreeksstatus = UUID.randomUUID (). ToString (); request.getSession (). setAttribute ("CLIENT_LOCAL_STATE", staat);

Vervolgens halen we de configuratie-informatie van de client op:

String autorisatieUri = 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);

Vervolgens voegen we deze stukjes informatie als queryparameters toe aan het autorisatie-eindpunt van de autorisatieserver:

String autorisatieLocation = autorisatieUri + "? Response_type = code" + "& client_id =" + clientId + "& redirect_uri =" + redirectUri + "& scope =" + scope + "& state =" + state;

En tot slot leiden we de browser om naar deze URL:

response.sendRedirect (autorisatieLocatie);

Na het verwerken van het verzoek, het autorisatie-eindpunt van de autorisatieserver genereert een code en voegt deze toe, naast de ontvangen statusparameter, aan de redirect_uri en zal de browser terugverwijzen // localhost: 9081 / callback? code = A123 & state = Y.

4.3. Toegangstokenverzoek

De client-callback-servlet, /Bel terug, begint met het valideren van het ontvangen staat:

String localState = (String) request.getSession (). GetAttribute ("CLIENT_LOCAL_STATE"); if (! localState.equals (request.getParameter ("state"))) {request.setAttribute ("error", "Het state attribuut komt niet overeen!"); verzending ("/", verzoek, antwoord); terugkeren; }

De volgende, we gebruiken de code die we eerder hebben ontvangen om een ​​toegangstoken aan te vragen via het tokeneindpunt van de autorisatieserver:

String code = request.getParameter ("code"); Client client = ClientBuilder.newClient (); WebTarget target = client.target (config.getValue ("provider.tokenUri", String.class)); Formulier = nieuw formulier (); form.param ("grant_type", "autorisatiecode"); 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 (formulier, MediaType.APPLICATION_FORM_URLENCODED_TYPE).

Zoals we kunnen zien, is er geen browserinteractie voor deze oproep en wordt het verzoek rechtstreeks gedaan met behulp van de JAX-RS-client-API als een HTTP POST.

Omdat het token-eindpunt de clientverificatie vereist, hebben we de clientreferenties opgenomen klant identificatie en client_secret in de Autorisatie koptekst.

De cliënt kan dit toegangstoken gebruiken om de API's van de bronserver aan te roepen, die het onderwerp is van de volgende subsectie.

4.4. Beveiligde toegang tot bronnen

Op dit punt hebben we een geldig toegangstoken en kunnen we de /lezen en /schrijven API's.

Om dat te doen, we moeten de Autorisatie koptekst. Met behulp van de JAX-RS Client API wordt dit eenvoudig gedaan via de Invocation.Builder-koptekst () methode:

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

5. OAuth 2.0-bronserver

In deze sectie bouwen we een beveiligde webapplicatie op basis van JAX-RS, MicroProfile JWT en MicroProfile Config. De MicroProfile JWT zorgt voor het valideren van de ontvangen JWT en het toewijzen van de JWT-scopes aan Jakarta EE-rollen.

5.1. Afhankelijkheden van Maven

Naast de Java EE Web API-afhankelijkheid hebben we ook de MicroProfile Config en MicroProfile JWT API's nodig:

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

5.2. JWT-authenticatiemechanisme

De MicroProfile JWT biedt een implementatie van het Bearer Token Authentication-mechanisme. Dit zorgt voor de verwerking van het JWT dat aanwezig is in het Autorisatie header, stelt een Jakarta EE Security Principal beschikbaar als een JsonWebToken die de JWT-claims bevat en de scopes toewijst aan Jakarta EE-rollen. Bekijk de Jakarta EE Security API voor meer achtergrondinformatie.

Om het JWT-authenticatiemechanisme in de server, we moeten voeg de LoginConfig annotatie in de JAX-RS-applicatie:

@ApplicationPath ("/ api") @DeclareRoles ({"resource.read", "resource.write"}) @LoginConfig (authMethod = "MP-JWT") openbare klasse OAuth2ResourceServerApplication breidt applicatie uit {}

Bovendien, MicroProfile JWT heeft de openbare RSA-sleutel nodig om de JWT-handtekening te verifiëren. We kunnen dit bieden door introspectie of, voor de eenvoud, door de sleutel handmatig van de autorisatieserver te kopiëren. In beide gevallen moeten we de locatie van de openbare sleutel opgeven:

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

Ten slotte moet de MicroProfile JWT het iss claim van de inkomende JWT, die aanwezig moet zijn en overeenkomt met de waarde van de MicroProfile Config-eigenschap:

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

Dit is doorgaans de locatie van de autorisatieserver.

5.3. De beveiligde eindpunten

Voor demonstratiedoeleinden voegen we een resource-API met twee eindpunten toe. Een daarvan is een lezen eindpunt dat toegankelijk is voor gebruikers met het resource.read reikwijdte en nog een schrijven eindpunt voor gebruikers met resource.write reikwijdte.

De beperking van de scopes wordt gedaan via de @RollenAllowed annotatie:

@Path ("/ resource") @RequestScoped openbare klasse ProtectedResource {@Inject private JsonWebToken-principal; @GET @RolesAllowed ("resource.read") @Path ("/ read") public String read () {return "Beschermde bron benaderd door:" + principal.getName (); } @POST @RolesAllowed ("resource.write") @Path ("/ write") public String write () {return "Beschermde bron benaderd door:" + principal.getName (); }}

6. Alle servers draaien

Om één server te laten draaien, hoeven we alleen de Maven-opdracht in de bijbehorende map aan te roepen:

mvn pakket vrijheid: run-server

De autorisatieserver, de client en de bronserver zullen respectievelijk draaien en beschikbaar zijn op de volgende locaties:

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

We hebben dus toegang tot de startpagina van de klant en klikken vervolgens op "Toegangstoken ophalen" om de autorisatiestroom te starten. Nadat we het toegangstoken hebben ontvangen, hebben we toegang tot het lezen en schrijven API's.

Afhankelijk van de toegekende bereiken, zal de bronserver reageren met een succesvol bericht of we krijgen een HTTP 403 verboden status.

7. Conclusie

In dit artikel hebben we een implementatie gegeven van een OAuth 2.0 Authorization Server die kan worden gebruikt met elke compatibele OAuth 2.0 Client en Resource Server.

Om het algemene raamwerk uit te leggen, hebben we ook een implementatie voor de client en de bronserver verzorgd. Om al deze componenten te implementeren, hebben we Jakarta EE 8 API's gebruikt, vooral CDI, Servlet, JAX RS, Jakarta EE Security. Daarnaast hebben we de pseudo-Jakarta EE API's van de MicroProfile gebruikt: MicroProfile Config en MicroProfile JWT.

De volledige broncode voor de voorbeelden is beschikbaar op GitHub. Houd er rekening mee dat de code een voorbeeld bevat van zowel de autorisatiecode als de toekenningstypen voor vernieuwingstoken.

Ten slotte is het belangrijk om je bewust te zijn van het educatieve karakter van dit artikel en dat het gegeven voorbeeld niet in productiesystemen gebruikt mag worden.