{"id":21186,"date":"2024-12-06T12:02:29","date_gmt":"2024-12-06T11:02:29","guid":{"rendered":"https:\/\/trifork.nl\/blog\/?p=21186"},"modified":"2024-12-06T15:39:14","modified_gmt":"2024-12-06T14:39:14","slug":"dpop-oidc-with-spring-security","status":"publish","type":"post","link":"https:\/\/trifork.nl\/blog\/dpop-oidc-with-spring-security\/","title":{"rendered":"Supporting Okta with DPoP in an OIDC-based Spring Security application"},"content":{"rendered":"\n<h1 class=\"wp-block-heading\">Dear Joris,<\/h1>\n\n\n\n<p>At my current company we\u2019re using Spring Security for authentication and authorization purposes. We were using an internal OpenID Connect provider to let end-users log in, but now the whole organization is forced to migrate to Okta. That may not sound like a problem (because hey, standards, right?), but the global security team has decreed that all new applications connecting to Okta must support this thing called <em>DPoP<\/em>. Pfff, until we got the memo I hadn\u2019t even heard of DPoP!<br>From what I gather it\u2019s mainly targeted at public clients like single-page applications. However, now even our traditional server-side web apps that let clients log in via Okta no longer work and it looks like the framework doesn\u2019t provide support for this new standard yet.<\/p>\n\n\n\n<p>Dear Joris, I don\u2019t know what to do: my manager is asking me every day when I will have resolved this blocking issue, but I don\u2019t even know where to start! Can you please offer some of your advice? Ideally for free?<\/p>\n\n\n\n<p><em>Restless in Rotterdam<\/em><\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Dear Restless in Rotterdam,<\/h1>\n\n\n\n<p>I can see how this issue is keeping you awake at night: this \u201cDemonstrating Proof of Possession\u201d standard is pretty new and indeed not yet supported when you configure Spring Security with OIDC.&nbsp;<br>However, please keep in mind that Spring Security is a framework: it has been created with extensibility in mind, so the solution lies in finding the right extension points to then implement the necessary support yourself.<\/p>\n\n\n\n<p>As I can imagine that other readers of this advice column are struggling with the same issue that you\u2019re facing, I\u2019ve created a sample application that shows how to go about this. Fear not: I\u2019m sure that with my help you\u2019ll be able to solve your problems in no time! Well, the DPoP-related ones, at least.&nbsp;<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Demonstrating <em>what<\/em> and <em>how<\/em>, exactly?<\/h2>\n\n\n\n<p>Proof of Possession means that you can prove that when you\u2019re sending JWTs with OIDC, they really came from you and weren\u2019t leaked to be used for things like replay attacks. There\u2019s plenty of info on the web that provides more background, but for Okta you should at least read <a href=\"https:\/\/developer.okta.com\/docs\/guides\/dpop\/oktaresourceserver\/main\/\" target=\"_blank\" rel=\"noreferrer noopener\">their documentation on their support<\/a>.<\/p>\n\n\n\n<p>What you can see there is that in order to successfully interact with the <code>\/token<\/code> endpoint, you have to add a DPoP request header with a JWT that\u2019s signed using a dedicated RSA-based JWK.<br>When doing that for the first time, the server will respond with a 400 Bad Request that contains a nonce as a response header. You then need to repeat your request with that nonce incorporated as a claim in your signed JWT. You should cache that nonce and refresh it every 24 hours.<\/p>\n\n\n\n<p>When you\u2019ve managed to do that, you\u2019re not done yet. A Spring Security application using OIDC will also interact with a <code>\/userinfo<\/code> endpoint if the response of the <code>\/token<\/code> endpoint includes some of the expected scopes. For that, DPoP requires you to use the returned access token as an \u201c<code>Authorization: DPoP &lt;token&gt;<\/code>\u201d header AND to send a DPoP header with an additional \u201c<code>ath<\/code>\u201d claim containing the Base64-encoded SHA-256 hash of that same access token (which is an Okta-specific requirement, BTW).<\/p>\n\n\n\n<p>If you\u2019re just feeling yourself tumbling into the pit of despair after reading all of that, fear not: I\u2019ll walk you through the required steps.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Sign on the dotted line<\/h2>\n\n\n\n<p>At the core of DPoP is the creation of the signed JWT that\u2019s your DPoP header value. Let\u2019s create a token manager that knows how to do that.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: java; title: ; notranslate\" title=\"\">\n@Component\npublic class DpopTokenManager {\n\n    private final Logger logger = LoggerFactory.getLogger(getClass());\n    private final JWSSignerFactory jwsSignerFactory = new DefaultJWSSignerFactory();\n\n    private volatile JWK jwk;\n    private volatile String nonce;\n\n    \/**\n     * Should be reset every 24 hours.\n     * @see &lt;a href=&quot;https:\/\/developer.okta.com\/docs\/guides\/dpop\/nonoktaresourceserver\/main\/#configure-dpop&quot;&gt;Okta docs&lt;\/a&gt;\n     *\/\n    @Scheduled(fixedDelay = 1000L * 60L * 60L * 24L)\n    public void resetNonce() {\n        this.nonce = null;\n        logger.info(&quot;DPoP nonce reset&quot;);\n    }\n\n    public void setNonce(String nonce) {\n        this.nonce = nonce;\n    }\n\n    \/\/ ...\n    \n    private String generateDpopToken(RequestEntity&lt;?&gt; request, String ath) {\n        try {\n            \/\/ could also be fixated via config using RSAKey.parse()\n            if (this.jwk == null) {\n                this.jwk = new RSAKeyGenerator(2048).keyID(&quot;1&quot;).generate();\n            }\n            var claimsBuilder = new JWTClaimsSet.Builder()\n                    .claim(&quot;htu&quot;, request.getUrl().toString())\n                    .claim(&quot;htm&quot;, request.getMethod().toString())\n                    .issueTime(new Date())\n                    .jwtID(UUID.randomUUID().toString());\n            if (ath != null) {\n                claimsBuilder.claim(&quot;ath&quot;, ath);\n            } else if (this.nonce != null) {\n                claimsBuilder.claim(&quot;nonce&quot;, this.nonce);\n            }\n            var signedJWT = new SignedJWT(\n                    new JWSHeader.Builder(JWSAlgorithm.RS256)\n                            .jwk(this.jwk.toPublicJWK())\n                            .type(new JOSEObjectType(&quot;dpop+jwt&quot;))\n                            .build(),\n                    claimsBuilder.build());\n            signedJWT.sign(jwsSignerFactory.createJWSSigner(this.jwk));\n            return signedJWT.serialize();\n        } catch (JOSEException e) {\n            throw new RuntimeException(&quot;Can't generate DpopToken&quot;, e);\n        }\n    }\n}\n<\/pre><\/div>\n\n\n<p>Spring Security\u2019s OAuth 2 client support comes with a dependency on the <a href=\"https:\/\/bitbucket.org\/connect2id\/nimbus-jose-jwt\/src\/master\/\" target=\"_blank\" rel=\"noreferrer noopener\">Nimbus JOSE + JWT library<\/a>. We\u2019re using that to generate an RSA-based JWK if one isn\u2019t explicitly configured and to create and sign a JWT with the header and claims that we need for DPoP.&nbsp;<\/p>\n\n\n\n<p>We\u2019ll add methods to this class to enrich the HTTP requests we send to Okta in a moment.&nbsp;<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Go with the flow<\/h2>\n\n\n\n<p>We\u2019ll start to implement first steps of the DPoP flow as shown in Okta\u2019s documentation:<\/p>\n\n\n\n<p><img loading=\"lazy\" decoding=\"async\" width=\"624\" height=\"447\" src=\"https:\/\/lh7-rt.googleusercontent.com\/docsz\/AD_4nXeTYu-i2dhcOWx0b2TtuxlZkaNggJocSbJBlmbhV60KRhqtt4qX2ZSa2kNOaoTxeucjXu8wG_wALKkrPPchAkoalH9_7mZuVQ8YhgImhpJYMReEWJCAe4e2HYO4wMz6uDreyc9KYQ?key=-DGiwD3sGNBWSBM23nx2dJRY\"><\/p>\n\n\n\n<p>Note that Okta acts as the Authorization server in our case and our own application is the OIDC client. That\u2019s because our security is handled in the backend application, rather than an application client.&nbsp;<br>Step 1 has been implemented in the code already shown. So where do we start to add support for the other steps?<\/p>\n\n\n\n<p>It turns out that in Spring Security, the steps used to obtain an access and ID token are coordinated by an <code>OAuth2AccessTokenResponseClient<\/code>. The default implementation for that is the <code>DefaultAuthorizationCodeTokenResponseClient<\/code> (or, in the latest Spring Security version which deprecates that class, the <code>RestClientAuthorizationCodeTokenResponseClient<\/code>; I\u2019ll focus on the former for this column, as most readers won\u2019t be on the very latest yet).<br>That default has no support for DPoP, however, and assumes that it\u2019ll receive a Bearer token rather than a DPoP token as a response. Let\u2019s fix that by providing our own implementation, based on the default.<\/p>\n\n\n\n<p>What do we need to customize? As it turns out, several things:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Add the DPoP header to our HTTP request;<\/li>\n\n\n\n<li>Respond to the nonce-challenge by extracting and caching the nonce and making a second request with an updated DPoP header;<\/li>\n\n\n\n<li>Ensure that we can handle a response with a DPoP token rather than a Bearer token, as Spring Security expects the latter;<\/li>\n\n\n\n<li>Similarly, be able to handle errors communicated in a WWW-Authenticate response header that start with \u201cDPoP\u201d rather than \u201cBearer\u201d, as Spring Security only supports the latter by default.<\/li>\n<\/ul>\n\n\n\n<p>All moving parts of the framework that handle the regular OIDC flow can be replaced through configuration. Here\u2019s an implementation:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: java; title: ; notranslate\" title=\"\">\n@Component\npublic class DpopAuthorizationCodeTokenResponseClient implements OAuth2AccessTokenResponseClient&lt;OAuth2AuthorizationCodeGrantRequest&gt; {\n    private static final String INVALID_TOKEN_RESPONSE_ERROR_CODE = &quot;invalid_token_response&quot;;\n\n    private final DpopTokenManager tokenManager;\n    private final OAuth2AuthorizationCodeGrantRequestEntityConverter requestEntityConverter \n        = new OAuth2AuthorizationCodeGrantRequestEntityConverter();\n    private final RestTemplate restTemplate;\n\n    public DpopAuthorizationCodeTokenResponseClient(DpopTokenManager tokenManager) {\n        this.tokenManager = tokenManager;\n        var oAuth2AccessTokenResponseHttpMessageConverter = new OAuth2AccessTokenResponseHttpMessageConverter();\n        oAuth2AccessTokenResponseHttpMessageConverter.setAccessTokenResponseConverter(\n            new CustomTokenResponseConverter());\n        this.restTemplate = new RestTemplate(List.of(\n                new FormHttpMessageConverter(), oAuth2AccessTokenResponseHttpMessageConverter));\n        restTemplate.setErrorHandler(new DPopAwareOAuth2ErrorResponseErrorHandler());\n    }\n\n    @Override\n    public OAuth2AccessTokenResponse getTokenResponse(OAuth2AuthorizationCodeGrantRequest authorizationGrantRequest) {\n        RequestEntity&lt;?&gt; request = requestEntityConverter.convert(authorizationGrantRequest);\n        ResponseEntity&lt;OAuth2AccessTokenResponse&gt; response;\n        try {\n            try {\n                response = this.restTemplate.exchange(tokenManager.withDpopHeader(request), OAuth2AccessTokenResponse.class);\n            } catch (DpopChallengeExceptionException e) {\n                tokenManager.setNonce(e.nonce);\n                response = this.restTemplate.exchange(tokenManager.withDpopHeader(request), OAuth2AccessTokenResponse.class);\n            }\n        } catch (RestClientException ex) {\n            var oauth2Error = new OAuth2Error(INVALID_TOKEN_RESPONSE_ERROR_CODE,\n                    &quot;An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response: &quot; + ex.getMessage(), null);\n            throw new OAuth2AuthorizationException(oauth2Error, ex);\n        }\n        OAuth2AccessTokenResponse tokenResponse = response.getBody();\n        Assert.notNull(tokenResponse,\n                &quot;The authorization server responded to this Authorization Code grant request with an empty body; as such, it cannot be materialized into an OAuth2AccessTokenResponse instance. Please check the HTTP response code in your server logs for more details.&quot;);\n        return tokenResponse;\n    }\n}\n<\/pre><\/div>\n\n\n<p>Let\u2019s break that down a bit. In the constructor we\u2019re configuring a custom access token response converter to be used by the HTTP message converter of our <code>RestTemplate<\/code>. The reason for that is that the <code>DefaultMapOAuth2AccessTokenResponseConverter<\/code> doesn\u2019t work for DPoP tokens, as it only accepts Bearer tokens. This custom converter basically does the same work, but hardcodes the <code>TokenType<\/code> to <code>Bearer<\/code> even if it isn\u2019t because the framework doesn\u2019t have a built-in type for DPoP just yet (but there\u2019s an <a href=\"https:\/\/github.com\/spring-projects\/spring-security\/issues\/14915#issuecomment-2473797900\" target=\"_blank\" rel=\"noreferrer noopener\">open issue to address this<\/a>).<\/p>\n\n\n\n<p>We also configure a custom error handler for that <code>RestTemplate<\/code>, which recognizes DPoP-specific errors. See below for the code of that error handler.<\/p>\n\n\n\n<p>The <code>getTokenResponse<\/code> method implements the flow as shown in the diagram. Like the default implementation, it converts the <code>OAuth2AuthorizationCodeGrantRequest<\/code> into a <code>RequestEntity<\/code> for sending to Okta. However, before actually sending the request it\u2019s passed to a \u201c<code>withDpopHeader<\/code>\u201d method of the injected <code>TokenManager<\/code>.<br>That method is quite straightforward to code, it looks like this:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: java; title: ; notranslate\" title=\"\">\npublic RequestEntity&lt;?&gt; withDpopHeader(RequestEntity&lt;?&gt; request) {\n   HttpHeaders headers = new HttpHeaders(request.getHeaders());\n   headers.set(&quot;DPoP&quot;, generateDpopToken(request, null));\n   return new RequestEntity&lt;&gt;(\n           request.getBody(),\n           headers,\n           request.getMethod(),\n           request.getUrl());\n}\n<\/pre><\/div>\n\n\n<p>Our custom error handler will throw a custom <code>DpopChallengeExceptionException<\/code> containing the nonce from the error response if we don\u2019t include a nonce, to which we respond by telling the <code>TokenManager<\/code> to use that nonce and retry the request.<\/p>\n\n\n\n<p>Everything else is just copied from the default implementation.&nbsp;<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Facing your problems<\/h2>\n\n\n\n<p>I mentioned our custom error handler. It needs to do two things:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Most importantly it needs to recognize the error response containing the nonce to use;<\/li>\n\n\n\n<li>In addition, it should also be able to extract errors returned in a WWW-Authenticate error. The default error handler only does this for values starting with \u201cBearer\u201d, not&nbsp; \u201cDPoP\u201d.<\/li>\n<\/ul>\n\n\n\n<p>Here\u2019s what that looks like (you could also use delegation instead of inheritance, if that makes you feel better about yourself):<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: java; title: ; notranslate\" title=\"\">\npublic class DPopAwareOAuth2ErrorResponseErrorHandler extends OAuth2ErrorResponseErrorHandler {\n   @Override\n   public void handleError(ClientHttpResponse errorResponse) throws IOException {\n       if (errorResponse.getStatusCode() == HttpStatus.BAD_REQUEST) {\n           OAuth2Error oauth2Error = readErrorFromWwwAuthenticate(errorResponse.getHeaders());\n           if (oauth2Error != null) {\n               throw new OAuth2AuthorizationException(oauth2Error);\n           }\n           String nonce = errorResponse.getHeaders().getFirst(&quot;dpop-nonce&quot;);\n           if (nonce != null) {\n               throw new DpopChallengeExceptionException(nonce);\n           }\n       }\n       super.handleError(errorResponse);\n   }\n\n   \/**\n    * Based on {@link OAuth2ErrorResponseErrorHandler#readErrorFromWwwAuthenticate(HttpHeaders)},\n    * but for DPoP-based errors.\n    *\/\n   private OAuth2Error readErrorFromWwwAuthenticate(HttpHeaders headers) {\n       \/\/ check for DPoP error in WWW-Authenticate response header\n       String wwwAuthenticateHeader = headers.getFirst(HttpHeaders.WWW_AUTHENTICATE);\n       if (StringUtils.hasText(wwwAuthenticateHeader) &amp;&amp; wwwAuthenticateHeader.startsWith(&quot;DPoP&quot;)) {\n           try {\n               DPoPTokenError tokenError = DPoPTokenError.parse(wwwAuthenticateHeader);\n               String errorCode = tokenError.getCode() != null ? tokenError.getCode() : OAuth2ErrorCodes.SERVER_ERROR;\n               String errorDescription = tokenError.getDescription();\n               String errorUri = tokenError.getURI() != null ? tokenError.getURI().toString() : null;\n\n               return new OAuth2Error(errorCode, errorDescription, errorUri);\n           } catch (Exception e) {\n               \/\/ ignore, let parent handle this\n           }\n       }\n       return null;\n   }\n}\n<\/pre><\/div>\n\n\n<h2 class=\"wp-block-heading\">Getting some closure<\/h2>\n\n\n\n<p>OK, so what\u2019s left? Let\u2019s look at the Okta diagram:<br><br><img loading=\"lazy\" decoding=\"async\" width=\"624\" height=\"332\" src=\"https:\/\/lh7-rt.googleusercontent.com\/docsz\/AD_4nXfaBc9CtmnbeyJcLQOZYzXhVqznctXdueZxsp_pqSff6bF1MEz7U7Aa9VxGig8sm64VAIXLlXOXd7CnexhHHxzRsDJPMpO1G6HzzA82CD874lhVo7YJTUdYOqT6Q-b8YK4YoTUL?key=-DGiwD3sGNBWSBM23nx2dJRY\"><\/p>\n\n\n\n<p>In our case, the Authorization server and Okta are the same. This is because our security is handled on the backend, as mentioned already.<\/p>\n\n\n\n<p>In our <code>TokenManager<\/code> we already have code that can add the <code>ath<\/code> claim, but how do we obtain it and set it at the right time, i.e. when the <code>\/userinfo<\/code> endpoint is called?<\/p>\n\n\n\n<p>The component responsible for this is the <code>requestEntityConverter<\/code> of the <code>DefaultOAuth2UserService<\/code>. Its default implementation can be replaced with a DPoP-aware version, which looks like this:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: java; title: ; notranslate\" title=\"\">\n@Component\npublic class DPopAwareOAuth2UserRequestEntityConverter implements Converter&lt;OAuth2UserRequest, RequestEntity&lt;?&gt;&gt; {\n   private final OAuth2UserRequestEntityConverter delegate = new OAuth2UserRequestEntityConverter();\n   private final DpopTokenManager tokenManager;\n\n   public DPopAwareOAuth2UserRequestEntityConverter(DpopTokenManager tokenManager) {\n       this.tokenManager = tokenManager;\n   }\n\n   @Override\n   public RequestEntity&lt;?&gt; convert(OAuth2UserRequest oAuth2UserRequest) {\n       RequestEntity&lt;?&gt; request = delegate.convert(oAuth2UserRequest);\n       return tokenManager.withDpopAndAuthorizationHeader(request, oAuth2UserRequest.getAccessToken());\n   }\n}\n<\/pre><\/div>\n\n\n<p>Similarly to the code in the access token response client, we\u2019re using the result of the default Spring Security code and process that before we send the resulting <code>RequestEntity<\/code> to Okta.\u00a0<br>The implementation of the additional <code>TokenManager<\/code> method looks like this, per Okta\u2019s documentation:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: java; title: ; notranslate\" title=\"\">\npublic RequestEntity&lt;?&gt; withDpopAndAuthorizationHeader(RequestEntity&lt;?&gt; request, OAuth2AccessToken accessToken) {\n   HttpHeaders headers = new HttpHeaders(request.getHeaders());\n   headers.set(&quot;Authorization&quot;, &quot;DPoP &quot; + accessToken.getTokenValue());\n\n   try {\n       byte&#x5B;] hash = MessageDigest.getInstance(&quot;SHA-256&quot;).digest(\n               accessToken.getTokenValue().getBytes(UTF_8));\n       headers.set(&quot;DpOP&quot;, generateDpopToken(request, Base64URL.encode(hash).toString()));\n   } catch (NoSuchAlgorithmException e) {\n       throw new RuntimeException(e);\n   }\n   return new RequestEntity&lt;&gt;(\n           request.getBody(),\n           headers,\n           request.getMethod(),\n           request.getUrl());\n}\n<\/pre><\/div>\n\n\n<p>Final step is to configure all of this in our security configuration:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: java; title: ; notranslate\" title=\"\">\n@Configuration\npublic class SecurityConfig  {\n\n   @Bean\n   SecurityFilterChain securityFilterChain(\n      HttpSecurity http,\n      DpopAuthorizationCodeTokenResponseClient authorizationCodeTokenResponseClient,\n      DPopAwareOAuth2UserRequestEntityConverter requestEntityConverter) throws Exception \n   {\n       return http\n           .authorizeHttpRequests(auth -&gt;\n               auth.anyRequest().authenticated())\n           .oauth2Login(login -&gt;\n               login.tokenEndpoint(tep -&gt;\n                   tep.accessTokenResponseClient(authorizationCodeTokenResponseClient))\n               .userInfoEndpoint(uie -&gt;\n                   uie.oidcUserService(dpopAwareOidcUserService(requestEntityConverter))))\n           .build();\n   }\n\n   OidcUserService dpopAwareOidcUserService(DPopAwareOAuth2UserRequestEntityConverter requestEntityConverter) {\n       var oAuth2UserService = new DefaultOAuth2UserService();\n       oAuth2UserService.setRequestEntityConverter(requestEntityConverter);\n\n       var restTemplateForOAuth2UserService = new RestTemplate();\n       restTemplateForOAuth2UserService.setErrorHandler(new DPopAwareOAuth2ErrorResponseErrorHandler());\n       oAuth2UserService.setRestOperations(restTemplateForOAuth2UserService);\n\n       var oidcUserService = new OidcUserService();\n       oidcUserService.setOauth2UserService(oAuth2UserService);\n       return oidcUserService;\n   }\n<\/pre><\/div>\n\n\n<h2 class=\"wp-block-heading\">In Conclusion<\/h2>\n\n\n\n<p>Dear Restless in Rotterdam, I hope that this elaborate answer has given you enough clues to implement a solution. This is, however, an advice column and not a professional consultation. <br>Should you require more help from an expert in the field, then don\u2019t hesitate to<a href=\"https:\/\/trifork.nl\/contact\/\" target=\"_blank\" rel=\"noreferrer noopener\"> contact me<\/a>!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Dear Joris, At my current company we\u2019re using Spring Security for authentication and authorization purposes. We were using an internal OpenID Connect provider to let end-users log in, but now the whole organization is forced to migrate to Okta. That may not sound like a problem (because hey, standards, right?), but the global security team [&hellip;]<\/p>\n","protected":false},"author":62,"featured_media":21187,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"content-type":"","footnotes":""},"categories":[337,94],"tags":[71,228],"class_list":["post-21186","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-from-the-trenches","category-spring","tag-security","tag-spring-security"],"acf":[],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v24.4 - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>Supporting Okta with DPoP in an OIDC-based Spring Security application - Trifork Blog<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/trifork.nl\/blog\/dpop-oidc-with-spring-security\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Supporting Okta with DPoP in an OIDC-based Spring Security application - Trifork Blog\" \/>\n<meta property=\"og:description\" content=\"Dear Joris, At my current company we\u2019re using Spring Security for authentication and authorization purposes. We were using an internal OpenID Connect provider to let end-users log in, but now the whole organization is forced to migrate to Okta. That may not sound like a problem (because hey, standards, right?), but the global security team [&hellip;]\" \/>\n<meta property=\"og:url\" content=\"https:\/\/trifork.nl\/blog\/dpop-oidc-with-spring-security\/\" \/>\n<meta property=\"og:site_name\" content=\"Trifork Blog\" \/>\n<meta property=\"article:published_time\" content=\"2024-12-06T11:02:29+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2024-12-06T14:39:14+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/trifork.nl\/blog\/wp-content\/uploads\/sites\/3\/2024\/12\/restless_i_am__i_cannot_create_imagination_again_by_aihybrid_dgaweqm-pre.jpg\" \/>\n\t<meta property=\"og:image:width\" content=\"1182\" \/>\n\t<meta property=\"og:image:height\" content=\"676\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/jpeg\" \/>\n<meta name=\"author\" content=\"Joris Kuipers\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Joris Kuipers\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"7 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\/\/trifork.nl\/blog\/dpop-oidc-with-spring-security\/\",\"url\":\"https:\/\/trifork.nl\/blog\/dpop-oidc-with-spring-security\/\",\"name\":\"Supporting Okta with DPoP in an OIDC-based Spring Security application - Trifork Blog\",\"isPartOf\":{\"@id\":\"https:\/\/trifork.nl\/blog\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/trifork.nl\/blog\/dpop-oidc-with-spring-security\/#primaryimage\"},\"image\":{\"@id\":\"https:\/\/trifork.nl\/blog\/dpop-oidc-with-spring-security\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/trifork.nl\/blog\/wp-content\/uploads\/sites\/3\/2024\/12\/restless_i_am__i_cannot_create_imagination_again_by_aihybrid_dgaweqm-pre.jpg\",\"datePublished\":\"2024-12-06T11:02:29+00:00\",\"dateModified\":\"2024-12-06T14:39:14+00:00\",\"author\":{\"@id\":\"https:\/\/trifork.nl\/blog\/#\/schema\/person\/265bd41e503f7176742258a927de598b\"},\"breadcrumb\":{\"@id\":\"https:\/\/trifork.nl\/blog\/dpop-oidc-with-spring-security\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/trifork.nl\/blog\/dpop-oidc-with-spring-security\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/trifork.nl\/blog\/dpop-oidc-with-spring-security\/#primaryimage\",\"url\":\"https:\/\/trifork.nl\/blog\/wp-content\/uploads\/sites\/3\/2024\/12\/restless_i_am__i_cannot_create_imagination_again_by_aihybrid_dgaweqm-pre.jpg\",\"contentUrl\":\"https:\/\/trifork.nl\/blog\/wp-content\/uploads\/sites\/3\/2024\/12\/restless_i_am__i_cannot_create_imagination_again_by_aihybrid_dgaweqm-pre.jpg\",\"width\":1182,\"height\":676},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/trifork.nl\/blog\/dpop-oidc-with-spring-security\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/trifork.nl\/blog\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Supporting Okta with DPoP in an OIDC-based Spring Security application\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/trifork.nl\/blog\/#website\",\"url\":\"https:\/\/trifork.nl\/blog\/\",\"name\":\"Trifork Blog\",\"description\":\"Keep updated on the technical solutions Trifork is working on!\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/trifork.nl\/blog\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Person\",\"@id\":\"https:\/\/trifork.nl\/blog\/#\/schema\/person\/265bd41e503f7176742258a927de598b\",\"name\":\"Joris Kuipers\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/trifork.nl\/blog\/#\/schema\/person\/image\/\",\"url\":\"https:\/\/secure.gravatar.com\/avatar\/9ab8da0d60582bad84342d4602d23dbd?s=96&d=mm&r=g\",\"contentUrl\":\"https:\/\/secure.gravatar.com\/avatar\/9ab8da0d60582bad84342d4602d23dbd?s=96&d=mm&r=g\",\"caption\":\"Joris Kuipers\"},\"sameAs\":[\"http:\/\/www.trifork.nl\"],\"url\":\"https:\/\/trifork.nl\/blog\/author\/jorisk\/\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Supporting Okta with DPoP in an OIDC-based Spring Security application - Trifork Blog","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/trifork.nl\/blog\/dpop-oidc-with-spring-security\/","og_locale":"en_US","og_type":"article","og_title":"Supporting Okta with DPoP in an OIDC-based Spring Security application - Trifork Blog","og_description":"Dear Joris, At my current company we\u2019re using Spring Security for authentication and authorization purposes. We were using an internal OpenID Connect provider to let end-users log in, but now the whole organization is forced to migrate to Okta. That may not sound like a problem (because hey, standards, right?), but the global security team [&hellip;]","og_url":"https:\/\/trifork.nl\/blog\/dpop-oidc-with-spring-security\/","og_site_name":"Trifork Blog","article_published_time":"2024-12-06T11:02:29+00:00","article_modified_time":"2024-12-06T14:39:14+00:00","og_image":[{"width":1182,"height":676,"url":"https:\/\/trifork.nl\/blog\/wp-content\/uploads\/sites\/3\/2024\/12\/restless_i_am__i_cannot_create_imagination_again_by_aihybrid_dgaweqm-pre.jpg","type":"image\/jpeg"}],"author":"Joris Kuipers","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Joris Kuipers","Est. reading time":"7 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/trifork.nl\/blog\/dpop-oidc-with-spring-security\/","url":"https:\/\/trifork.nl\/blog\/dpop-oidc-with-spring-security\/","name":"Supporting Okta with DPoP in an OIDC-based Spring Security application - Trifork Blog","isPartOf":{"@id":"https:\/\/trifork.nl\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/trifork.nl\/blog\/dpop-oidc-with-spring-security\/#primaryimage"},"image":{"@id":"https:\/\/trifork.nl\/blog\/dpop-oidc-with-spring-security\/#primaryimage"},"thumbnailUrl":"https:\/\/trifork.nl\/blog\/wp-content\/uploads\/sites\/3\/2024\/12\/restless_i_am__i_cannot_create_imagination_again_by_aihybrid_dgaweqm-pre.jpg","datePublished":"2024-12-06T11:02:29+00:00","dateModified":"2024-12-06T14:39:14+00:00","author":{"@id":"https:\/\/trifork.nl\/blog\/#\/schema\/person\/265bd41e503f7176742258a927de598b"},"breadcrumb":{"@id":"https:\/\/trifork.nl\/blog\/dpop-oidc-with-spring-security\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/trifork.nl\/blog\/dpop-oidc-with-spring-security\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/trifork.nl\/blog\/dpop-oidc-with-spring-security\/#primaryimage","url":"https:\/\/trifork.nl\/blog\/wp-content\/uploads\/sites\/3\/2024\/12\/restless_i_am__i_cannot_create_imagination_again_by_aihybrid_dgaweqm-pre.jpg","contentUrl":"https:\/\/trifork.nl\/blog\/wp-content\/uploads\/sites\/3\/2024\/12\/restless_i_am__i_cannot_create_imagination_again_by_aihybrid_dgaweqm-pre.jpg","width":1182,"height":676},{"@type":"BreadcrumbList","@id":"https:\/\/trifork.nl\/blog\/dpop-oidc-with-spring-security\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/trifork.nl\/blog\/"},{"@type":"ListItem","position":2,"name":"Supporting Okta with DPoP in an OIDC-based Spring Security application"}]},{"@type":"WebSite","@id":"https:\/\/trifork.nl\/blog\/#website","url":"https:\/\/trifork.nl\/blog\/","name":"Trifork Blog","description":"Keep updated on the technical solutions Trifork is working on!","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/trifork.nl\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Person","@id":"https:\/\/trifork.nl\/blog\/#\/schema\/person\/265bd41e503f7176742258a927de598b","name":"Joris Kuipers","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/trifork.nl\/blog\/#\/schema\/person\/image\/","url":"https:\/\/secure.gravatar.com\/avatar\/9ab8da0d60582bad84342d4602d23dbd?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/9ab8da0d60582bad84342d4602d23dbd?s=96&d=mm&r=g","caption":"Joris Kuipers"},"sameAs":["http:\/\/www.trifork.nl"],"url":"https:\/\/trifork.nl\/blog\/author\/jorisk\/"}]}},"_links":{"self":[{"href":"https:\/\/trifork.nl\/blog\/wp-json\/wp\/v2\/posts\/21186","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/trifork.nl\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/trifork.nl\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/trifork.nl\/blog\/wp-json\/wp\/v2\/users\/62"}],"replies":[{"embeddable":true,"href":"https:\/\/trifork.nl\/blog\/wp-json\/wp\/v2\/comments?post=21186"}],"version-history":[{"count":7,"href":"https:\/\/trifork.nl\/blog\/wp-json\/wp\/v2\/posts\/21186\/revisions"}],"predecessor-version":[{"id":21195,"href":"https:\/\/trifork.nl\/blog\/wp-json\/wp\/v2\/posts\/21186\/revisions\/21195"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/trifork.nl\/blog\/wp-json\/wp\/v2\/media\/21187"}],"wp:attachment":[{"href":"https:\/\/trifork.nl\/blog\/wp-json\/wp\/v2\/media?parent=21186"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/trifork.nl\/blog\/wp-json\/wp\/v2\/categories?post=21186"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/trifork.nl\/blog\/wp-json\/wp\/v2\/tags?post=21186"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}