OAuth/JWT-autentisering - #232
Conversation
The client can now authenticate in two ways: the existing certificate-based signing (Signer), or OAuth 2.0 client credentials with a certificate-bound JWT (RFC 8705) over mutual TLS. - JwtAuthConfig: configures the token endpoint, resource server, clientId and client certificate (PKCS12 keystore or KeyStore). - MutualTlsTokenProvider: fetches and caches an access token from mIdP over mTLS, with a refresh margin and expiry derived from expires_in or the exp claim. - RequestBearerTokenInterceptor: sets the Authorization:Bearer header. - ApiServiceImpl selects the authentication mode based on whether a Signer or a JwtAuthConfig is set, and throws when neither is configured. - New DigipostClient constructors without a Signer.
Moving this out of RequestSignatureInterceptor, as we need this functionality also for OAuth-based authentication, which does not use the RequestSignatureInterceptor. Also defined the attribute name as a constant in the new interceptor to make the connection between the interceptor and verification step clearer.
The client previously selected its authentication mode based on whether a Signer was null, and the JWT/mTLS config was hidden inside DigipostClientConfig. The choice was scattered and easy to misconfigure. - Introduce DigipostClient.withCertificateAuthentication(...) and withJwtMtlsAuthentication(...) (each with an HttpClientBuilder overload), so the chosen authentication method is stated at the call site and the required credential cannot be forgotten. - Replace the implicit "signer == null" selection with an explicit AuthMode enum resolved in one place (ApiServiceImpl#resolveAuthMode). It now also throws when both certificate and JWT/mTLS auth are configured, or neither. - Move JwtAuthConfig out of DigipostClientConfig; it is now a required argument to the JWT factory method. - Add Javadoc for the factory methods, including the clientBuilder parameter. - Add ApiServiceImplAuthModeTest covering all four resolution cases. BREAKING CHANGE: the DigipostClient(config, brokerId, signer[, clientBuilder]) constructors and the implicit no-signer constructors have been removed. All call sites (example code and DigipostSwingClient) have been migrated to the new factory methods.
Every versioned docs page carried redirect_from: /, so several versions claimed the site root and the target became ambiguous. Drop it from the older versions so only v19 (the current version) owns the root redirect.
The resource URI could be set both in JwtAuthConfig.apiUri and DigipostClientConfig.digipostApiUri, both defaulting to production. A client pointed at test through DigipostClientConfig alone got tokens for production.
Token endpoint failures surfaced as IllegalStateException, so callers could not handle them like the rest of the client's errors. Also keep the cause when the response is not valid JSON.
The test built its own SSLContext and HTTP client, so it verified the test's handshake rather than the provider's. It passed even with the provider's logic untouched. Let the provider's trust managers be overridden, so the test can drive the real getToken() path: mTLS handshake, request parameters, caching, expiry from the exp claim, and error mapping.
The enum and the unreachable switch default guarded against states only the internal constructor could create. The factories make the mode a property of the call, so no argument has to be null.
The interceptors were handed the eventLogger already wrapped for ApiServiceImpl's logger, so every message reached slf4j twice.
Taking a Supplier<String> instead of MutualTlsTokenProvider lets the interceptor be tested without a keystore and a TLS handshake.
The name stuttered "Request" twice.
Also fixes "certificate-base" -> "certificate-based", and states on the JWT methods that tokens are requested for the API given by config.
martin-jackson
left a comment
There was a problem hiding this comment.
Heftig shit!
Sliter litt med å få testet med testklienter: Fikk først kompilseringsfeil på JwtAuthConfig::newBuilder, men ser ut som at det bare er pga commit 872aa89.
Etter å ha builderen til følgende får jeg 401 fra midp:
JwtAuthConfig jwtAuthConfig = JwtAuthConfig
.newConfig(clientId)
.tokenEndpoint(midpTokenUri.toString())
.pkcs12KeyStore(keyStream, keyPassword.getPassword())
.build();Exception in thread "main" no.digipost.api.client.errorhandling.DigipostClientException: FAILED_TO_OBTAIN_ACCESS_TOKEN: Token endpoint returned HTTP 401 for https://midp.qa.digipost.no/oauth2/token: {"error_description":"Client authentication failed: client_id","error":"invalid_client","error_uri":"https://datatracker.ietf.org/doc/html/rfc6749#section-3.2.1"}
at no.digipost.api.client.security.jwt.MutualTlsTokenProvider.lambda$fetchAndCacheToken$0(MutualTlsTokenProvider.java:107)
at org.apache.hc.client5.http.impl.classic.CloseableHttpClient.execute(CloseableHttpClient.java:247)
at org.apache.hc.client5.http.impl.classic.CloseableHttpClient.execute(CloseableHttpClient.java:188)
at org.apache.hc.client5.http.impl.classic.CloseableHttpClient.execute(CloseableHttpClient.java:162)
at no.digipost.api.client.security.jwt.MutualTlsTokenProvider.fetchAndCacheToken(MutualTlsTokenProvider.java:103)
at no.digipost.api.client.security.jwt.MutualTlsTokenProvider.getToken(MutualTlsTokenProvider.java:90)
at no.digipost.api.client.internal.http.request.interceptor.RequestBearerTokenInterceptor.process(RequestBearerTokenInterceptor.java:36)
at org.apache.hc.core5.http.protocol.DefaultHttpProcessor.process(DefaultHttpProcessor.java:107)
| .setSslContext(sslContext) | ||
| .build()) | ||
| .build()); | ||
| } |
There was a problem hiding this comment.
Mogleg det er nice å kunne konfigurere timeouts på denne klienten, nå er den på 10 sek fra defaultsa.
Hilsen en som har brent seg på lange timeouts mer enn én gang
| } | ||
| } else { | ||
| setSignatureHeader(httpRequest); | ||
| private static void verifyContentIsHashed(HttpRequest httpRequest) { |
There was a problem hiding this comment.
Vet ikke om det er meningen, men denne sjekken ser ut til å bare bli gjort for cert-auth fordi RequestSignatureInterceptor ikke er lagt til i createJwtAuthenticatingHttpClient().
Headeren X-Content-SHA256 ser ut til å være påkrevd for både cert og jwt i dpost-api
|
|
||
| private final JwtAuthConfig config; | ||
| private final Clock clock; | ||
| private final CloseableHttpClient tokenClient; |
There was a problem hiding this comment.
Pirkings: Mangler close av tokenClient
Mulig databasen har blitt resatt, skal få dyttet inn ny testdata |
arneroen
left a comment
There was a problem hiding this comment.
Ref meldinga i midp-kanalen på slack her for leden. Vi må bli enige om hvordan vi håndterer 401 fra API-serveren. Minimum tenker jeg at det cachede tokenet bør invalideres. Og så tror jeg helt fint vi kan hente et nytt token og retrye requesten mot APIet.
| - **Certificate-based signing:** each request is signed with a private key. Use | ||
| `DigipostClient.withCertificateAuthentication(...)`. | ||
|
|
||
| The chosen method is stated explicitly in the factory method you call. |
There was a problem hiding this comment.
Ønsker vi her å fremheve JWT-authen som det "korrekte" valget? Feks ved å kalle den signatur-baserte løsningen "legacy"?
| } | ||
|
|
||
| public ApiServiceImpl(DigipostClientConfig config, HttpClientBuilder httpClientBuilder, BrokerId brokerId, Signer signer, JwtAuthConfig jwtAuthConfig) { | ||
| public static ApiServiceImpl withJwtMtlsAuthentication(DigipostClientConfig config, HttpClientBuilder httpClientBuilder, BrokerId brokerId, JwtAuthConfig jwtAuthConfig) { |
There was a problem hiding this comment.
Litt nitpick, men disse metodenavnene kan antyde at auth-metoden mot API-serveren er JWT+mTLS, som jo ikke stemmer.
|
|
||
| CloseableHttpClient httpClient = httpClientBuilder | ||
| .setConnectionManager(HttpClientConnectionManagerFactory.createDefaultBuilder() | ||
| .setSSLSocketFactory(SSLConnectionSocketFactoryBuilder.create() |
There was a problem hiding this comment.
setSSLSocketFactory og SSLConnectionSocketFactoryBuilder er deprecated. Et raskt google-søk foreslår å bruke TlsSocketStrategy.
| Clock clock = config.clock; | ||
| MutualTlsTokenProvider tokenProvider = new MutualTlsTokenProvider(jwtAuthConfig, brokerId, config.digipostApiUri, clock); | ||
|
|
||
| CloseableHttpClient httpClient = httpClientBuilder |
There was a problem hiding this comment.
Den samme HTTP-klienten brukes mot både mIdPen og mot API-serveren. Denne klienten er konfigurert for mTLS.
Så lenge digipost sitt API ikke også er konfigurert for mTLS, vil jo ikke klient -> API kommunisere over mTLS. Dette i henhold til det dokumentasjonen sier. Men dersom mTLS også slås på i digipost APIet, vi klienten forsøke å kommunisere over mTLS, som jo strider med dokumentasjonen.
Eventuelt er intensjonen kanskje at klient -> digipost-API også skal gå over mTLS, ref RFC 8705?
| (Digipost OAuth 2 client authority (Nyva))[https://nyva.digipost.no]. Contact the sales team at Digipost to get access to | ||
| the client authority and register your client. More information can be found in the (Digipost API Documentation)[https://digipost.github.io/digipost-technical-docs/]. |
There was a problem hiding this comment.
Feil markdown i disse to lenkene, skal være (text)[url]
OAuth 2-autentisering mot Digipost API
Legger til funksjonalitet slik at klienter kan autentisere seg mot Digipost API med JWT, med OAuth-klienter registrert i Nyva og tokens hentet fra mIdP'en.
Denne PR'en er altså klientsiden av denne PR'en i digipost/digipost, som allerede er i prod.
Fremangsmåte
Jeg ville gått gjennom commit for commit og lest commitmeldingene, men ikke nødvendigvis allt koden da det har vært en del frem og tilbake her.
Klassene jeg ville fokusert på er:
Kort oppsummert
Bakoverkompabilitet
Endringene er bakoverkompatible med eksisterende autentiseringsløsning. Valg av autentiseringsmetode gjøres nå med factorymetodene
Henting og caching av tokens
Klienten håndterer henting og caching av tokens selv. "Batteries included", som jeg sikkert hadde sagt om jeg ikke var så jordnær og ydmyk som jeg jo er.
Testing
Jeg har også laget en demo i testklienter, på branch test-jwt-autentisering. Der finner man testen
no.digipost.testclients.dpostapi.oauth2.OAuth2SendeKlientQa, som tester sending av dokumenter med JWT-autentisering mot QA.