Implementering af OAuth 2.0 autorisationsrammen ved hjælp af Jakarta EE

1. Oversigt

I denne vejledning skal vi give en implementering af OAuth 2.0 Authorization Framework ved hjælp af Jakarta EE og MicroProfile. Vigtigst af alt skal vi implementere interaktionen mellem OAuth 2.0-roller gennem tildelingstypen Autorisationskode. Motivationen bag denne skrivning er at yde støtte til projekter, der implementeres ved hjælp af Jakarta EE, da dette endnu ikke yder støtte til OAuth.

For den vigtigste rolle skal autorisationsserveren, Vi implementerer autorisationsendepunktet, token-slutpunktet og derudover JWK-nøgleendepunktet, hvilket er nyttigt for ressource-serveren til at hente den offentlige nøgle.

Da vi ønsker, at implementeringen skal være enkel og let for en hurtig opsætning, skal vi bruge en forudregistreret butik af klienter og brugere og naturligvis en JWT-butik til adgangstokener.

Før du hopper lige ind i emnet, er det vigtigt at bemærke, at eksemplet i denne vejledning er til uddannelsesmæssige formål. Til produktionssystemer anbefales det stærkt at bruge en moden, velafprøvet løsning som Keycloak.

2. OAuth 2.0 Oversigt

I dette afsnit vil vi give en kort oversigt over OAuth 2.0-roller og tildelingsflow for autorisationskoden.

2.1. Roller

OAuth 2.0-rammen indebærer et samarbejde mellem de fire følgende roller:

  • Ressourceejer: Normalt er dette slutbrugeren - det er den enhed, der har nogle ressourcer, der er værd at beskytte
  • Ressource server: En tjeneste, der beskytter ressourceejerens data, som normalt offentliggøres via en REST API
  • Klient: Et program, der bruger ressourceejerens data
  • Autorisationsserver: En applikation, der giver tilladelse - eller autoritet - til klienter i form af udløbende tokens

2.2. Typer af godkendelsestilskud

EN tilskudstype er, hvordan en klient får tilladelse til at bruge ressourceejerens data, i sidste ende i form af et adgangstoken.

Naturligvis foretrækker forskellige typer klienter forskellige typer tilskud:

  • Autorisationskode: Foretrukket oftestom det er en webapplikation, en indbygget applikation eller en enkelt-sideprogram, selvom indfødte apps og apps på en side kræver yderligere beskyttelse kaldet PKCE
  • Opdater Token: Et særligt fornyelsesstipendium, velegnet til webapplikationer for at forny deres eksisterende token
  • Oplysninger om klienter: Foretrukket for service-til-service kommunikation, sig, når ressourceejeren ikke er en slutbruger
  • RessourceejerAdgangskode: Foretrukket for førstepartsgodkendelse af native applikationer, sig, når mobilappen har brug for sin egen login-side

Derudover kan klienten bruge implicit tilskudstype. Det er dog normalt mere sikkert at bruge tildelingen af ​​autorisationskoden med PKCE.

2.3. Autorisationskode Grant Flow

Da tildelingsstrømmen for autorisationskoden er den mest almindelige, lad os også gennemgå, hvordan det fungerer, og det er faktisk det, vi bygger i denne vejledning.

En applikation - en klient - anmoder om tilladelse ved at omdirigere til autorisationsserveren /bemyndige slutpunkt. Til dette slutpunkt giver applikationen en ring tilbage slutpunkt.

Autorisationsserveren beder normalt slutbrugeren - ressourceejeren - om tilladelse. Hvis slutbrugeren giver tilladelse, så autorisationsserveren omdirigerer tilbage til tilbagekaldet med en kode.

Ansøgningen modtager denne kode og derefter foretager et godkendt opkald til autorisationsserveren /polet slutpunkt. Ved "godkendt" mener vi, at applikationen beviser, hvem det er som en del af dette opkald. Hvis alt vises i rækkefølge, reagerer autorisationsserveren med tokenet.

Med symbolet i hånden applikationen fremsætter sin anmodning til API'en - ressource serveren - og at API'en verificerer tokenet. Det kan bede autorisationsserveren om at bekræfte tokenet ved hjælp af dets / introspekt slutpunkt. Eller hvis tokenet er selvstændigt, kan ressource-serveren optimeres ved lokal verifikation af tokens underskrift, som det er tilfældet med JWT.

2.4. Hvad understøtter Jakarta EE?

Ikke meget endnu. I denne vejledning bygger vi de fleste ting fra bunden.

3. OAuth 2.0 autorisationsserver

I denne implementering vil vi fokusere på den mest anvendte tilskudstype: Autorisationskode.

3.1. Registrering af klient og bruger

En autorisationsserver skal selvfølgelig have kendskab til klienterne og brugerne, før den kan godkende deres anmodninger. Og det er almindeligt, at en autorisationsserver har et brugergrænseflade til dette.

For enkelheds skyld bruger vi dog en forudkonfigureret klient:

INSERT INTO clients (client_id, client_secret, redirect_uri, scope, authorized_grant_types) VALUES ('webappclient', 'webappclientsecret', '// localhost: 9180 / callback', 'resource.read resource.write', 'authorisation_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") privat streng redirectUri; @Column (name = "scope") privat String scope; // ...}

Og en forudkonfigureret bruger:

INSERT INTO-brugere (user_id, password, roller, scopes) VALUES ('appuser', 'appusersecret', 'USER', 'resource.read resource.write');
@Entity @Table (name = "brugere") offentlig klasse Bruger implementerer Principal {@Id @Column (name = "user_id") private String userId; @Column (name = "password") privat strengadgangskode; @Kolonne (navn = "roller") private Stringroller; @Column (name = "scopes") private strengomfang; // ...}

Bemærk, at vi af denne vejledning har brugt adgangskoder i almindelig tekst, men i et produktionsmiljø skal de hashes.

For resten af ​​denne vejledning viser vi hvordan appuser - ressourceejeren - kan give adgang til webappclient - ansøgningen - ved at implementere autorisationskoden.

3.2. Autorisations slutpunkt

Godkendelsesendepunkts hovedrolle er først godkende brugeren og derefter bede om tilladelserne - eller rækkevidde - som applikationen ønsker.

Som instrueret i OAuth2-specifikationerne skal dette slutpunkt understøtte HTTP GET-metoden, selvom det også kan understøtte HTTP POST-metoden. I denne implementering understøtter vi kun HTTP GET-metoden.

Først, godkendelsesendepunktet kræver, at brugeren godkendes. Specifikationen kræver ikke en bestemt måde her, så lad os bruge formgodkendelse fra Jakarta EE 8 Security API:

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

Brugeren omdirigeres til /login.jsp til godkendelse og vil derefter være tilgængelig som en Opkaldsprincipal gennem SecurityContext API:

Principal principal = securityContext.getCallerPrincipal ();

Vi kan sammensætte disse ved hjælp af JAX-RS:

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

På dette tidspunkt kan godkendelsesendepunktet begynde at behandle applikationens anmodning, som skal indeholde respons_type og klient_id parametre og - valgfrit, men anbefalet - redirect_uri, omfang, og stat parametre.

Det klient_id skal være en gyldig klient, i vores tilfælde fra kunder databasetabel.

Det redirect_urihvis det er angivet, skal det også matche det, vi finder i kunder databasetabel.

Og fordi vi laver autorisationskode, respons_type er kode.

Da autorisation er en proces i flere trin, kan vi midlertidigt gemme disse værdier i sessionen:

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

Og forbered dig derefter på at spørge brugeren, hvilke tilladelser applikationen kan bruge, og omdirigere til den side:

Streng tilladtScopes = checkUserScopes (user.getScopes (), anmodetScope); request.setAttribute ("scopes", allowedScopes); request.getRequestDispatcher ("/ authorize.jsp"). videresend (anmodning, svar);

3.3. Godkendelse af brugeromfang

På dette tidspunkt giver browseren en brugergrænseflade til brugeren og brugeren foretager et valg. Derefter browseren sender brugerens valg i en 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 original (MultivaluedMap). "ORIGINAL_PARAMS"); // ... Streng godkendelseStatus = params.getFirst ("godkendelsesstatus"); // JA ELLER NEJ // ... hvis JA-listen er godkendtSkoper = params.get ("scope"); // ...}

Derefter genererer vi en midlertidig kode, der refererer til bruger-id, klient-id, ogredirect_uri, som alle applikationen bruger senere, når den rammer token-slutpunktet.

Så lad os oprette en Autorisationskode JPA-enhed med en automatisk genereret id:

@Entity @Table (navn) offentlig klasse AuthorizationCode {@Id @GeneratedValue (strategi = GenerationType.AUTO) @Column (navn = "kode") privat strengkode; // ...}

Og derefter udfylde det:

AuthorizationCode authorCode = ny AuthorizationCode (); authorisationCode.setClientId (clientId); authorisationCode.setUserId (userId); authorisationCode.setApprovedScopes (String.join ("", autoriserede Scope)); authorisationCode.setExpirationDate (LocalDateTime.now (). plusMinutes (2)); authorisationCode.setRedirectUri (redirectUri);

Når vi gemmer bønnen, udfyldes kodeattributten automatisk, og så kan vi hente den og sende den tilbage til klienten:

appDataRepository.save (autorisationskode); Strengkode = autorisationskode.getkode ();

Noter det vores autorisationskode udløber om to minutter - vi skal være så konservative som muligt med denne udløb. Det kan være kort, da klienten udveksler det med det samme med et adgangstoken.

Vi omdirigerer derefter tilbage til applikationens redirect_uri, giver den koden såvel som enhver stat parameter, som applikationen har angivet i dens /bemyndige anmodning:

StringBuilder sb = ny StringBuilder (redirectUri); // ... sb.append ("? code ="). append (kode); Strengtilstand = params.getFirst ("tilstand"); if (state! = null) {sb.append ("& state ="). append (state); } URI-placering = UriBuilder.fromUri (sb.toString ()). Build (); returnere Response.seeOther (placering) .build ();

Bemærk igen det omdirigereUri er hvad der eksisterer i kunder bord, ikke redirect_uri anmodningsparameter.

Så vores næste trin er, at klienten modtager denne kode og udveksler den med et adgangstoken ved hjælp af token-slutpunktet.

3.4. Token slutpunkt

I modsætning til godkendelsesendepunktet er tokenendepunktet behøver ikke en browser for at kommunikere med klienten, og vi implementerer det derfor som et JAX-RS-slutpunkt:

@Path ("token") offentlig klasse TokenEndpoint {List supportedGrantTypes = Collections.singletonList ("autorisationskode"); @Injicér privat AppDataRepository appDataRepository; @Inject Instance authorisationGrantTypeHandlers; @POST @Produces (MediaType.APPLICATION_JSON) @Consumes (MediaType.APPLICATION_FORM_URLENCODED) public Response token (MultivaluedMap params, @HeaderParam (HttpHeaders.AUTHORIZATION) String authHeader) kaster JOSEException // ...

Token-slutpunktet kræver en POST samt kodning af parametrene ved hjælp af application / x-www-form-urlencoded medietype.

Som vi diskuterede, vil vi kun støtte autorisationskode tilskudstype:

Liste understøttetGrantTypes = Collections.singletonList ("autorisationskode");

Så den modtagne tilskudstype som en krævet parameter skal understøttes:

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:" + supportGrantTypes). build (); returnere Response.status (Response.Status.BAD_REQUEST) .entity (fejl) .build (); }

Dernæst kontrollerer vi klientgodkendelse via HTTP Basic-godkendelse. Det vil sige, vi kontrollerer hvis modtaget klient_id og klienthemmelighed, gennem Bemyndigelse header, matcher en registreret klient:

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

Endelig delegerer vi produktionen af TokenResponse til en tilsvarende bevillingshåndterer:

offentlig grænseflade AuthorizationGrantTypeHandler {TokenResponse createAccessToken (String clientId, MultivaluedMap params) kaster Undtagelse; }

Da vi er mere interesserede i tildelingstypen for autorisationskode, har vi leveret en passende implementering som en CDI-bønne og dekoreret den med Som hedder kommentar:

@Named ("autorisation_kode")

Ved runtime og i henhold til den modtagne tilskudstype værdi aktiveres den tilsvarende implementering via CDI Instance-mekanismen:

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

Det er nu tid til at producere /polet'S svar.

3.5. RSA Private og offentlige nøgler

Før vi genererer tokenet, har vi brug for en privat RSA-nøgle til signering af tokens.

Til dette formål bruger vi OpenSSL:

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

Det private-key.pem leveres til serveren via MicroProfile Config signeringstast ejendom ved hjælp af filen META-INF / microprofile-config. egenskaber:

signaturnøgle = / META-INF / private-key.pem

Serveren kan læse ejendommen ved hjælp af den injicerede Konfig objekt:

String signaturnøgle = config.getValue ("signaturnøgle", String.class);

På samme måde kan vi generere den tilsvarende offentlige nøgle:

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

Og brug MicroProfile Config verificationKey at læse det:

verifikationsnøgle = / META-INF / public-key.pem

Serveren skal gøre den tilgængelig for ressource-serveren til formålet med verifikation. Dette er gjort gennem et JWK-slutpunkt.

Nimbus JOSE + JWT er et bibliotek, der kan være en stor hjælp her. Lad os først tilføje nimbus-jose-jwt afhængighed:

 com.nimbusds nimbus-jose-jwt 7.7 

Og nu kan vi udnytte Nimbus's JWK-support for at forenkle vores slutpunkt:

@Path ("jwk") @ApplicationScoped public class JWKEndpoint {@GET public Response getKey (@QueryParam ("format") Stringformat) kaster undtagelse {// ... String verificationkey = config.getValue ("verification key", String. klasse); String pemEncodedRSAPublicKey = PEMKeyUtils.readKeyAsString (verifikationsnøgle); hvis (format == null || format.equals ("jwk")) {JWK jwk = JWK.parseFromPEMEncodedObjects (pemEncodedRSAPublicKey); returnere Response.ok (jwk.toJSONString ()). type (MediaType.APPLICATION_JSON) .build (); } ellers hvis (format.equals ("pem")) {return Response.ok (pemEncodedRSAPublicKey) .build (); } // ...}}

Vi har brugt formatet parameter for at skifte mellem PEM- og JWK-formaterne. MicroProfile JWT, som vi bruger til implementering af ressource-serveren, understøtter begge disse formater.

3.6. Token Endpoint Response

Det er nu tid til en given AuthorizationGrantTypeHandler for at oprette token-svaret. I denne implementering understøtter vi kun de strukturerede JWT-tokens.

For at oprette et token i dette format bruger vi igen Nimbus JOSE + JWT-biblioteket, men der er også mange andre JWT-biblioteker.

Så for at oprette en signeret JWT, vi skal først konstruere JWT-overskriften:

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

Derefter bygger vi nyttelasten hvilket er en Sæt af standardiserede og brugerdefinerede krav:

Øjeblikkelig nu = Øjeblikkelig. Nu (); Lang udløberInMin = 30L; Dato in30Min = Date.from (now.plus (udløberInMin, ChronoUnit.MINUTES)); JWTClaimsSet jwtClaims = ny JWTClaimsSet.Builder () .issuer ("// localhost: 9080") .subject (authorisationCode.getUserId ()) .claim ("upn", authorisationCode.getUserId ()) .audience ("// localhost: 9280 ") .claim (" scope ", authorisationCode.getApprovedScopes ()) .claim (" groups ", Arrays.asList (authorisationCode.getApprovedScopes (). Split (" "))). ExpirationTime (in30Min) .notBeforeTime (Date. fra (nu)) .issueTime (Date.from (now)) .jwtID (UUID.randomUUID (). toString ()) .build (); SignedJWT signeretJWT = ny SignedJWT (jwsHeader, jwtClaims);

Ud over de almindelige JWT-krav har vi tilføjet to yderligere krav - upn og grupper - som MicroProfile JWT har brug for. Det upn vil blive kortlagt til Jakarta EE Security Opkaldsprincipal og grupper vil blive kortlagt til Jakarta EE Roller.

Nu hvor vi har overskriften og nyttelasten, vi er nødt til at underskrive adgangstokenet med en RSA privat nøgle. Den tilsvarende RSA-offentlige nøgle vil blive eksponeret gennem JWK-slutpunktet eller blive gjort tilgængelig på andre måder, så ressource-serveren kan bruge den til at verificere adgangstokenet.

Da vi har leveret den private nøgle som et PEM-format, skal vi hente den og omdanne den til en RSAPrivateKey:

SignedJWT signeretJWT = ny SignedJWT (jwsHeader, jwtClaims); // ... String signaturnøgle = config.getValue ("signaturnøgle", String.class); Streng pemEncodedRSAPrivateKey = PEMKeyUtils.readKeyAsString (signaturnøgle); RSAKey rsaKey = (RSAKey) JWK.parseFromPEMEncodedObjects (pemEncodedRSAPrivateKey);

Næste, vi underskriver og serierer JWT:

signeret JWT.sign (ny RSASSASigner (rsaKey.toRSAPrivateKey ())); Streng accessToken = signeretJWT.serialize ();

Og endelig vi konstruerer et tokensvar:

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

som, takket være JSON-P, serieliseres til JSON-format og sendes til klienten:

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

4. OAuth 2.0-klient

I dette afsnit vil vi være opbygning af en webbaseret OAuth 2.0-klient ved hjælp af Servlet, MicroProfile Config og JAX RS Client API'er.

Mere præcist implementerer vi to hovedservlets: en til at anmode om autorisationsservers godkendelsesendepunkt og få en kode ved hjælp af tildelingstypen for autorisationskode og en anden servlet til at bruge den modtagne kode og anmode om et adgangstoken fra autorisationsservers tokenendepunkt .

Derudover implementerer vi yderligere to servlets: En til at få et nyt adgangstoken ved hjælp af tildelingstypen for opdateringstoken og en anden til at få adgang til ressource serverens API'er.

4.1. OAuth 2.0-klientoplysninger

Da klienten allerede er registreret i autorisationsserveren, skal vi først angive klientregistreringsoplysningerne:

  • klient-id: Client Identifier, og den udstedes normalt af autorisationsserveren under registreringsprocessen.
  • klienthemmelighed: Klienthemmelighed.
  • redirect_uri: Placering, hvor autorisationskoden skal modtages.
  • rækkevidde: Kunden anmodede om tilladelser.

Derudover skal klienten kende autorisationsservers godkendelse og token-slutpunkter:

  • autorisation_uri: Placering af godkendelsesservergodkendelsesendepunktet, som vi kan bruge til at få en kode.
  • token_uri: Placering af autorisationsservertokenens slutpunkt, som vi kan bruge til at få et token.

Al denne information leveres gennem MicroProfile Config-filen, META-INF / microprofile-config. egenskaber:

# Klientregistrering 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. Anmodning om autorisationskode

Strømmen med at få en autorisationskode starter med klienten ved at omdirigere browseren til autorisationsservers godkendelsesendepunkt.

Dette sker typisk, når brugeren forsøger at få adgang til en beskyttet ressource-API uden autorisation eller ved eksplicit ved at påkalde klienten /bemyndige sti:

@WebServlet (urlPatterns = "/ authorize") offentlig klasse AuthorizationCodeServlet udvider HttpServlet {@Inject private Config config; @ Override beskyttet ugyldigt doGet (HttpServletRequest anmodning, HttpServletResponse svar) kaster ServletException, IOException {// ...}}

I doGet () metode starter vi med at generere og gemme en sikkerhedstilstandsværdi:

Strengtilstand = UUID.randomUUID (). ToString (); request.getSession (). setAttribute ("CLIENT_LOCAL_STATE", tilstand);

Derefter henter vi klientkonfigurationsoplysningerne:

Streng autorisationUri = 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);

Vi tilføjer derefter disse stykker information som forespørgselsparametre til autorisationsservers godkendelsesendepunkt:

Streng autorisationLocation = autorisationUri + "? Respons_type = kode" + "& client_id =" + clientId + "& redirect_uri =" + redirectUri + "& scope =" + scope + "& state =" + state;

Og endelig omdirigerer vi browseren til denne URL:

respons.sendRedirect (authorisationLocation);

Efter behandling af anmodningen autorisationsservers godkendelsesendepunkt genererer og tilføjer en kode, ud over den modtagne tilstandsparameter, til redirect_uri og omdirigerer browseren tilbage // localhost: 9081 / callback? code = A123 & state = Y.

4.3. Forespørgsel om adgangstoken

Servlet til tilbagekaldelse af klienten, /ring tilbage, begynder med at validere det modtagne stat:

String localState = (String) request.getSession (). GetAttribute ("CLIENT_LOCAL_STATE"); if (! localState.equals (request.getParameter ("state"))) {request.setAttribute ("error", "The state attribute matches not!"); forsendelse ("/", anmodning, svar); Vend tilbage; }

Næste, vi bruger den kode, vi tidligere har modtaget til at anmode om et adgangstoken gennem autorisationsservers token-slutpunkt:

Strengkode = request.getParameter ("kode"); Klientklient = ClientBuilder.newClient (); WebTarget target = client.target (config.getValue ("provider.tokenUri", String.class)); Formularform = ny formular (); form.param ("grant_type", "autorisationskode"); form.param ("kode", kode); 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_TYesp;)

Som vi kan se, er der ingen browserinteraktion for dette opkald, og anmodningen foretages direkte ved hjælp af JAX-RS-klient-API'en som en HTTP-POST.

Da token-slutpunktet kræver klientgodkendelse, har vi inkluderet klientlegitimationsoplysningerne klient_id og klienthemmelighed i Bemyndigelse header.

Klienten kan bruge dette adgangstoken til at påkalde ressource server-API'erne, som er genstand for det næste underafsnit.

4.4. Beskyttet ressourceadgang

På dette tidspunkt har vi et gyldigt adgangstoken, og vi kan ringe til ressourceserverens /Læs og /skrive API'er.

At gøre det, vi er nødt til at give Bemyndigelse header. Ved hjælp af JAX-RS Client API gøres dette simpelthen via Invocation.Builder header () metode:

resourceWebTarget = webTarget.path ("ressource / læs"); Invocation.Builder invocationBuilder = resourceWebTarget.request (); respons = invocationBuilder .header ("autorisation", tokenResponse.getString ("access_token")) .get (String.class);

5. OAuth 2.0 ressource server

I dette afsnit bygger vi en sikret webapplikation baseret på JAX-RS, MicroProfile JWT og MicroProfile Config. MicroProfile JWT sørger for at validere den modtagne JWT og kortlægge JWT-omfanget til Jakarta EE-roller.

5.1. Maven afhængigheder

Ud over Java EE Web API-afhængighed har vi også brug for MicroProfile Config og MicroProfile JWT API'er:

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

5.2. JWT-godkendelsesmekanisme

MicroProfile JWT giver en implementering af Bearer Token Authentication-mekanismen. Dette tager sig af behandlingen af ​​JWT til stede i Bemyndigelse header, stiller Jakarta EE Security Principal til rådighed som en JsonWebToken som besidder JWT-kravene og kortlægger omfanget til Jakarta EE-roller. Se på Jakarta EE Security API for mere baggrund.

For at aktivere JWT-godkendelsesmekanisme på serveren, vi er nødt til tilføj LoginConfig kommentar i JAX-RS-applikationen:

@ApplicationPath ("/ api") @DeclareRoles ({"resource.read", "resource.write"}) @LoginConfig (authMethod = "MP-JWT") offentlig klasse OAuth2ResourceServerApplication udvider applikationen {}

Derudover MicroProfile JWT har brug for den offentlige RSA-nøgle for at verificere JWT-signaturen. Vi kan levere dette enten ved introspektion eller for at forenkle det ved manuelt at kopiere nøglen fra autorisationsserveren. I begge tilfælde skal vi angive placeringen af ​​den offentlige nøgle:

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

Endelig skal MicroProfile JWT verificere udstedelse krav fra den indgående JWT, som skal være til stede og matche værdien af ​​egenskaben MicroProfile Config:

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

Typisk er dette placeringen af ​​autorisationsserveren.

5.3. De sikrede slutpunkter

Til demonstrationsformål tilføjer vi en ressource-API med to slutpunkter. Den ene er en Læs slutpunkt, der er tilgængeligt for brugere, der har resource.read rækkevidde og en anden skrive slutpunkt for brugere med resource.write rækkevidde.

Begrænsningen af ​​omfanget sker gennem @RolesAllowed kommentar:

@Path ("/ resource") @RequestScoped offentlig klasse ProtectedResource {@Inject private JsonWebToken principal; @GET @RolesAllowed ("resource.read") @Path ("/ read") public Strenglæsning () {returner "Beskyttet ressource, der er adgang til:" + principal.getName (); } @POST @ RollesAllowed ("resource.write") @ Path ("/ skriv") offentlig String skriv () {returner "Beskyttet ressource tilgås af:" + principal.getName (); }}

6. Kørsel af alle servere

For at køre en server er vi bare nødt til at påkalde Maven-kommandoen i den tilsvarende mappe:

mvn-pakkefrihed: run-server

Autorisationsserveren, klienten og ressource-serveren kører og er tilgængelige henholdsvis på følgende placeringer:

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

Så vi kan få adgang til klientens startside, og derefter klikker vi på "Få adgangstoken" for at starte autorisationsstrømmen. Efter at have modtaget adgangstokenet kan vi få adgang til ressource-serverens Læs og skrive API'er.

Afhængigt af de tildelte anvendelsesområder reagerer ressursserveren enten ved en vellykket besked, eller vi får en HTTP 403-forbudt status.

7. Konklusion

I denne artikel har vi leveret en implementering af en OAuth 2.0 autorisationsserver, der kan bruges med enhver kompatibel OAuth 2.0-klient og ressourceserver.

For at forklare den overordnede ramme har vi også leveret en implementering til klienten og ressource-serveren. For at implementere alle disse komponenter har vi brugt Jakarta EE 8 API'er, især CDI, Servlet, JAX RS, Jakarta EE Security. Derudover har vi brugt pseudo-Jakarta EE API'erne fra MicroProfile: MicroProfile Config og MicroProfile JWT.

Den fulde kildekode til eksemplerne er tilgængelig på GitHub. Bemærk, at koden indeholder et eksempel på både tilladelseskoden og tildelingstyper til opdateringstoken.

Endelig er det vigtigt at være opmærksom på den artikels uddannelsesmæssige karakter, og at eksemplet ikke bør bruges i produktionssystemer.