OAuth2 voor een Spring REST API - Behandel de Refresh Token in Angular

1. Overzicht

In deze tutorial gaan we verder met het verkennen van de OAuth2-autorisatiecodestroom die we zijn begonnen samen te stellen in ons vorige artikel en we zullen ons concentreren op het omgaan met het vernieuwingstoken in een Angular-app. We maken ook gebruik van de Zuul-volmacht.

We gebruiken de OAuth-stack in Spring Security 5. Als je de verouderde Spring Security OAuth-stack wilt gebruiken, bekijk dan dit vorige artikel: OAuth2 voor een Spring REST API - Behandel de Refresh Token in AngularJS (oude OAuth-stack)

2. Toegangstoken verlopen

Onthoud allereerst dat de klant in twee stappen een toegangstoken heeft verkregen met behulp van een autorisatiecode-toekenningstype. In de eerste stap verkrijgen we de autorisatiecode. En in de tweede stap verkrijgen we daadwerkelijk het toegangstoken.

Ons Toegangstoken wordt opgeslagen in een cookie die vervalt op basis van wanneer het Token zelf verloopt:

var expireDate = nieuwe datum (). getTime () + (1000 * token.expires_in); Cookie.set ("access_token", token.access_token, expireDate);

Wat belangrijk is om te begrijpen, is dat de cookie zelf wordt alleen gebruikt voor opslag en het stuurt niets anders aan in de OAuth2-stroom. De browser zal de cookie bijvoorbeeld nooit automatisch naar de server sturen met verzoeken, dus we zijn hier beveiligd.

Maar merk op hoe we dit eigenlijk definiëren retrieveToken () functie om het toegangstoken te krijgen:

retrieveToken (code) {let params = nieuwe URLSearchParams (); params.append ('grant_type', 'autorisatiecode'); params.append ('client_id', this.clientId); params.append ('client_secret', 'newClientSecret'); params.append ('redirect_uri', this.redirectUri); params.append ('code', code); let headers = new HttpHeaders ({'Content-type': 'application / x-www-form-urlencoded; charset = utf-8'}); this._http.post ('// localhost: 8083 / auth / realms / baeldung / protocol / openid-connect / token', params.toString (), {headers: headers}) .subscribe (data => this.saveToken ( data), err => alert ('Ongeldige inloggegevens')); }

We sturen het klantgeheim in het params, wat niet echt een veilige manier is om hiermee om te gaan. Laten we eens kijken hoe we dit kunnen voorkomen.

3. De volmacht

Zo, we gaan nu een Zuul-proxy hebben die in de front-end-applicatie draait en in feite tussen de front-end-client en de autorisatieserver zit. Alle gevoelige informatie wordt op deze laag verwerkt.

De front-end client wordt nu gehost als een Boot-applicatie, zodat we naadloos kunnen verbinden met onze embedded Zuul-proxy met behulp van de Spring Cloud Zuul-starter.

Als je de basis van Zuul wilt bespreken, lees dan snel het hoofdartikel van Zuul.

Nu laten we de routes van de proxy configureren:

zuul: routes: auth / code: pad: / auth / code / ** sensitiveHeaders: url: // localhost: 8083 / auth / realms / baeldung / protocol / openid-connect / auth auth / token: pad: / auth / token / ** sensitiveHeaders: url: // localhost: 8083 / auth / realms / baeldung / protocol / openid-connect / token auth / refresh: pad: / auth / refresh / ** sensitiveHeaders: url: // localhost: 8083 / auth / realms / baeldung / protocol / openid-connect / token auth / redirect: path: / auth / redirect / ** sensitiveHeaders: url: // localhost: 8089 / auth / resources: path: / auth / resources / ** sensitiveHeaders: url: // localhost: 8083 / auth / resources /

We hebben routes opgesteld om het volgende af te handelen:

  • autorisatie code - verkrijg de autorisatiecode en sla deze op in een cookie
  • auth / redirect - afhandelen van de omleiding naar de inlogpagina van de autorisatieserver
  • auth / resources - toewijzen aan het corresponderende pad van de autorisatieserver voor de bronnen van de inlogpaginacss en js)
  • auth / token - verkrijg het toegangstoken, verwijder refresh_token van de payload en sla deze op in een cookie
  • auth / vernieuwen - haal het vernieuwingstoken op, verwijder het uit de payload en sla het op in een cookie

Wat hier interessant is, is dat we alleen verkeer naar de autorisatieserver proxy en niet naar iets anders. We hebben de proxy alleen echt nodig om binnen te komen als de klant nieuwe tokens verkrijgt.

Laten we deze allemaal een voor een bekijken.

4. Verkrijg de code met Zuul Pre Filter

Het eerste gebruik van de proxy is eenvoudig - we stellen een verzoek op om de autorisatiecode te krijgen:

@Component public class CustomPreZuulFilter breidt ZuulFilter uit {@Override public Object run () {RequestContext ctx = RequestContext.getCurrentContext (); HttpServletRequest req = ctx.getRequest (); String requestURI = req.getRequestURI (); if (requestURI.contains ("auth / code")) {Mapparams = ctx.getRequestQueryParams (); if (params == null) {params = Maps.newHashMap (); } params.put ("response_type", Lists.newArrayList (new String [] {"code"})); params.put ("scope", Lists.newArrayList (new String [] {"read"})); params.put ("client_id", Lists.newArrayList (nieuwe String [] {CLIENT_ID})); params.put ("redirect_uri", Lists.newArrayList (nieuwe String [] {REDIRECT_URL})); ctx.setRequestQueryParams (params); } retourneer null; } @Override openbare boolean shouldFilter () {boolean shouldfilter = false; RequestContext ctx = RequestContext.getCurrentContext (); String URI = ctx.getRequest (). GetRequestURI (); if (URI.contains ("auth / code") || URI.contains ("auth / token") || URI.contains ("auth / refresh")) {shouldfilter = true; } return shouldfilter; } @Override public int filterOrder () {return 6; } @Override public String filterType () {retourneer "pre"; }}

We gebruiken een filtertype van pre om het verzoek te verwerken alvorens het door te geven.

In het filter rennen() methode, voegen we queryparameters toe voor response_type, reikwijdte, klant identificatie en redirect_uri- alles wat onze autorisatieserver nodig heeft om ons naar zijn inlogpagina te brengen en een code terug te sturen.

Let ook op de shouldFilter () methode. We filteren alleen verzoeken met de 3 genoemde URI's, andere gaan niet door naar het rennen methode.

5. Zet de code in een cookie Gebruik makend van Zuul Postfilter

Wat we hier willen doen, is de code als een cookie opslaan, zodat we deze naar de autorisatieserver kunnen sturen om het toegangstoken op te halen. De code is aanwezig als een queryparameter in de verzoek-URL waarnaar de autorisatieserver ons verwijst na het inloggen.

We zetten een Zuul-postfilter op om deze code te extraheren en in de cookie te plaatsen. Dit is niet zomaar een normaal koekje, maar een beveiligde, alleen HTTP-cookie met een zeer beperkt pad (/ auth / token):

@Component openbare klasse CustomPostZuulFilter breidt ZuulFilter uit {private ObjectMapper mapper = new ObjectMapper (); @Override public Object run () {RequestContext ctx = RequestContext.getCurrentContext (); probeer {Map params = ctx.getRequestQueryParams (); if (requestURI.contains ("auth / redirect")) {Cookie cookie = nieuwe Cookie ("code", params.get ("code"). get (0)); cookie.setHttpOnly (true); cookie.setPath (ctx.getRequest (). getContextPath () + "/ auth / token"); ctx.getResponse (). addCookie (cookie); }} catch (uitzondering e) {logger.error ("Fout opgetreden in zuul post filter", e); } retourneer null; } @Override openbare boolean shouldFilter () {boolean shouldfilter = false; RequestContext ctx = RequestContext.getCurrentContext (); String URI = ctx.getRequest (). GetRequestURI (); if (URI.contains ("auth / redirect") || URI.contains ("auth / token") || URI.contains ("auth / refresh")) {shouldfilter = true; } return shouldfilter; } @Override public int filterOrder () {return 10; } @Override public String filterType () {retourneer "post"; }}

Om een ​​extra beschermingslaag tegen CSRF-aanvallen toe te voegen, we zullen een Same-Site-cookiekop aan al onze cookies toevoegen.

Daarvoor maken we een configuratieklasse aan:

@Configuration openbare klasse SameSiteConfig implementeert WebMvcConfigurer {@Bean openbare TomcatContextCustomizer sameSiteCookiesConfig () {retourcontext -> {laatste Rfc6265CookieProcessor cookieProcessor = nieuwe Rfc6265CookieProcessor (); cookieProcessor.setSameSiteCookies (SameSiteCookies.STRICT.getValue ()); context.setCookieProcessor (cookieProcessor); }; }}

Hier stellen we het attribuut in op streng, zodat elke overdracht van cookies naar andere sites strikt wordt geweigerd.

6. Verkrijg en gebruik de code van de cookie

Nu we de code in de cookie hebben, zal de front-end Angular-applicatie die een tokenverzoek probeert te activeren, het verzoek verzenden naar / auth / token en dus zal de browser die cookie natuurlijk verzenden.

Dus we hebben nu een andere aandoening in onze pre filter in de proxy dat zal de code uit de cookie halen en deze samen met andere formulierparameters verzenden om het token te verkrijgen:

openbaar object run () {RequestContext ctx = RequestContext.getCurrentContext (); ... else if (requestURI.contains ("auth / token"))) {probeer {String code = extractCookie (req, "code"); String formParams = String.format ("grant_type =% s & client_id =% s & client_secret =% s & redirect_uri =% s & code =% s", "autorisatiecode", CLIENT_ID, CLIENT_SECRET, REDIRECT_URL, code); byte [] bytes = formParams.getBytes ("UTF-8"); ctx.setRequest (nieuwe CustomHttpServletRequest (req, bytes)); } catch (IOException e) {e.printStackTrace (); }} ...} private String extractCookie (HttpServletRequest req, String naam) {Cookie [] cookies = req.getCookies (); if (cookies! = null) {for (int i = 0; i <cookies.length; i ++) {if (cookies [i] .getName (). equalsIgnoreCase (name)) {retour cookies [i] .getValue () ; }}} retourneer null; }

En hier is onzeCustomHttpServletRequest - gebruikt om ons verzoeklichaam te verzenden met de vereiste formulierparameters geconverteerd naar bytes:

openbare klasse CustomHttpServletRequest breidt HttpServletRequestWrapper {privé byte [] bytes uit; openbare CustomHttpServletRequest (verzoek HttpServletRequest, byte [] bytes) {super (verzoek); this.bytes = bytes; } @Override openbare ServletInputStream getInputStream () gooit IOException {retourneer nieuwe ServletInputStreamWrapper (bytes); } @Override public int getContentLength () {return bytes.length; } @Override public long getContentLengthLong () {return bytes.length; } @Override public String getMethod () {retourneer "POST"; }}

Hiermee krijgen we in het antwoord een toegangstoken van de autorisatieserver. Vervolgens zullen we zien hoe we het antwoord transformeren.

7. Plaats het vernieuwingstoken in een cookie

Op naar de leuke dingen.

Wat we hier willen doen, is dat de klant de Refresh Token als cookie krijgt.

We zullen toevoegen aan onze Zuul post-filter om de Refresh Token uit de JSON-body van het antwoord te extraheren en in de cookie in te stellen. Dit is wederom een ​​beveiligde, alleen HTTP-cookie met een zeer beperkt pad (/ auth / vernieuwen):

public Object run () {... else if (requestURI.contains ("auth / token") || requestURI.contains ("auth / refresh")) {InputStream is = ctx.getResponseDataStream (); String responseBody = IOUtils.toString (is, "UTF-8"); if (responseBody.contains ("refresh_token")) {Map responseMap = mapper.readValue (responseBody, nieuwe TypeReference() {}); String refreshToken = responseMap.get ("refresh_token"). ToString (); responseMap.remove ("refresh_token"); responseBody = mapper.writeValueAsString (responseMap); Cookie cookie = nieuwe Cookie ("refreshToken", refreshToken); cookie.setHttpOnly (true); cookie.setPath (ctx.getRequest (). getContextPath () + "/ auth / refresh"); cookie.setMaxAge (2592000); // 30 dagen ctx.getResponse (). AddCookie (cookie); } ctx.setResponseBody (responseBody); } ...}

Zoals we kunnen zien, hebben we hier een voorwaarde toegevoegd aan ons Zuul-postfilter om het antwoord te lezen en het Refresh Token voor de routes te extraheren auth / token en auth / vernieuwen. We doen exact hetzelfde voor de twee omdat de autorisatieserver in wezen dezelfde payload verzendt terwijl hij het toegangstoken en het vernieuwingstoken verkrijgt.

Daarna hebben we verwijderd refresh_token van het JSON-antwoord om ervoor te zorgen dat het nooit toegankelijk is voor de front-end buiten de cookie.

Een ander punt om op te merken is dat we de maximale leeftijd van de cookie instellen op 30 dagen - aangezien dit overeenkomt met de vervaltijd van het token.

8. Haal en gebruik het vernieuwingstoken van de cookie

Nu we het vernieuwingstoken in de cookie hebben, wanneer de front-end Angular-applicatie een tokenvernieuwing probeert te activeren, het gaat het verzoek sturen naar / auth / vernieuwen en dus zal de browser die cookie natuurlijk verzenden.

Dus we hebben nu een andere aandoening in onze pre filter in de proxy die het vernieuwingstoken uit de cookie haalt en het doorstuurt als een HTTP-parameter - zodat het verzoek geldig is:

openbaar object run () {RequestContext ctx = RequestContext.getCurrentContext (); ... else if (requestURI.contains ("auth / refresh"))) {probeer {String token = extractCookie (req, "token"); String formParams = String.format ("grant_type =% s & client_id =% s & client_secret =% s & refresh_token =% s", "refresh_token", CLIENT_ID, CLIENT_SECRET, token); byte [] bytes = formParams.getBytes ("UTF-8"); ctx.setRequest (nieuwe CustomHttpServletRequest (req, bytes)); } catch (IOException e) {e.printStackTrace (); }} ...}

Dit is vergelijkbaar met wat we deden toen we het toegangstoken voor het eerst kregen. Maar merk op dat het formulierlichaam anders is. Nu sturen we een grant_type van refresh_token in plaats van Authorisatie Code samen met het token dat we eerder in de cookie hadden opgeslagen.

Nadat het antwoord is verkregen, doorloopt het opnieuw dezelfde transformatie in het pre filter zoals we eerder in sectie 7 hebben gezien.

9. Het toegangstoken vernieuwen vanuit Angular

Laten we tot slot onze eenvoudige front-end-applicatie aanpassen en daadwerkelijk gebruik maken van het vernieuwen van het token:

Hier is onze functie refreshAccessToken ():

refreshAccessToken () {let headers = new HttpHeaders ({'Content-type': 'application / x-www-form-urlencoded; charset = utf-8'}); this._http.post ('auth / refresh', {}, {headers: headers}) .subscribe (data => this.saveToken (data), err => alert ('Invalid Credentials')); }

Merk op hoe we eenvoudig het bestaande gebruiken saveToken () functie - en er gewoon verschillende invoer aan doorgeven.

Merk dat ook op we voegen geen formulierparameters toe met de refresh_token onszelf - want dat wordt verzorgd door het Zuul-filter.

10. Voer de front-end uit

Omdat onze front-end Angular-client nu wordt gehost als een opstarttoepassing, zal het uitvoeren ervan iets anders zijn dan voorheen.

De eerste stap is hetzelfde. We moeten de app bouwen:

mvn schone installatie

Dit zal het frontend-maven-plugin gedefinieerd in onze pom.xml om de Angular-code te bouwen en de UI-artefacten naar target / klassen / statisch map. Dit proces overschrijft al het andere dat we in het src / main / resources directory. We moeten er dus voor zorgen dat alle vereiste bronnen uit deze map, zoals application.yml, tijdens het kopieerproces.

In de tweede stap moeten we onze SpringBootApplication klasse UiApplication. Onze client-app zal actief zijn op poort 8089 zoals gespecificeerd in het application.yml.

11. Conclusie

In deze OAuth2-tutorial hebben we geleerd hoe we het Refresh Token in een Angular-clienttoepassing kunnen opslaan, hoe we een verlopen Access Token kunnen vernieuwen en hoe we de Zuul-proxy daarvoor kunnen gebruiken.

De volledige implementatie van deze tutorial is te vinden op GitHub.