-
Notifications
You must be signed in to change notification settings - Fork 1
DPoP token service #31
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
11eefc7
f93ef05
a0e7bf2
1e53d7e
954aa99
76eff83
ac491b7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,39 +1,59 @@ | ||
| using Duende.AccessTokenManagement.OpenIdConnect; | ||
| using Duende.IdentityModel.Client; | ||
| using Fhi.Authentication.Setup; | ||
| using Microsoft.Extensions.Logging; | ||
|
|
||
| namespace Fhi.Authentication.OpenIdConnect | ||
| { | ||
| /// <summary> | ||
| /// Response for token validation. | ||
| /// </summary> | ||
| /// <param name="IsError"></param> | ||
| public record TokenResponse(bool IsError = false); | ||
| /// <summary> | ||
| /// Abstraction for token service. | ||
| /// TODO: response should be improved | ||
| /// </summary> | ||
| public record TokenResponse(string? AccessToken, bool IsError, string? ErrorDescription); | ||
| public interface ITokenService | ||
| { | ||
| /// <summary> | ||
| /// Refresh access token. | ||
| /// Create DPoP token | ||
| /// </summary> | ||
| /// <param name="refreshToken"></param> | ||
| /// <param name="authority">The OpenId connectprovider authority Url</param> | ||
| /// <param name="clientId">Client Identifier</param> | ||
| /// <param name="jwk">The private json web key for client assertion</param> | ||
| /// <param name="scopes">Separated list of scopes</param> | ||
| /// <param name="dPopJwk">The private json web key for DPoP</param> | ||
| /// <returns></returns> | ||
| Task<TokenResponse> RefreshAccessTokenAsync(string refreshToken); | ||
| public Task<TokenResponse> RequestDPoPToken(string authority, string clientId, string jwk, string scopes, string dPopJwk); | ||
| } | ||
|
|
||
| internal class DefaultTokenService(IUserTokenEndpointService UserTokenEndpointService, ILogger<DefaultTokenService> Logger) : ITokenService | ||
| public class TokenService( | ||
| ILogger<TokenService> Logger, | ||
| IHttpClientFactory HttpClientFactory) : ITokenService | ||
| { | ||
| public async Task<TokenResponse> RefreshAccessTokenAsync(string refreshToken) | ||
| public async Task<TokenResponse> RequestDPoPToken( | ||
| string authority, | ||
| string clientId, | ||
| string jwk, | ||
| string scopes, | ||
| string dPopJwk) | ||
| { | ||
| var userToken = await UserTokenEndpointService.RefreshAccessTokenAsync(new UserToken() { RefreshToken = refreshToken }, new UserTokenRequestParameters()); | ||
| if (userToken.IsError) | ||
| var client = HttpClientFactory.CreateClient(); | ||
| Logger.LogInformation("Get metadata from discovery endpoint from Authority {@Authority}", authority); | ||
| var discovery = await client.GetDiscoveryDocumentAsync(authority); | ||
| if (discovery is not null && !discovery.IsError && discovery.Issuer is not null && discovery.TokenEndpoint is not null) | ||
| { | ||
| Logger.LogError(message: userToken.Error); | ||
| return new TokenResponse(true); | ||
| var response = await client.RequestTokenWithDPoP(discovery, clientId, jwk, scopes, dPopJwk); | ||
|
|
||
| if (response.IsError && | ||
| response.Error == "use_dpop_nonce" && | ||
| response.HttpResponse?.Headers.Contains("DPoP-Nonce") == true) | ||
| { | ||
| var nonce = response.HttpResponse.Headers.GetValues("DPoP-Nonce").FirstOrDefault(); | ||
| if (!string.IsNullOrEmpty(nonce)) | ||
| { | ||
| response = await client.RequestTokenWithDPoP(discovery, clientId, jwk, scopes, dPopJwk, nonce); | ||
| } | ||
| } | ||
|
|
||
| return response.IsError ? | ||
| new TokenResponse(null, true, response.ErrorDescription) : | ||
| new TokenResponse(response.AccessToken, false, string.Empty); | ||
| } | ||
|
|
||
| return new TokenResponse(); | ||
| return new TokenResponse(null, true, discovery is null ? "No discovery document" : discovery.Error); | ||
| } | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| using Fhi.Authentication.OpenIdConnect; | ||
| using Duende.AccessTokenManagement.OpenIdConnect; | ||
| using Fhi.Authentication.OpenIdConnect; | ||
| using Microsoft.AspNetCore.Authentication.Cookies; | ||
| using Microsoft.Extensions.DependencyInjection; | ||
| using Microsoft.Extensions.Options; | ||
|
|
@@ -18,7 +19,8 @@ public static class ServiceCollectionExtensions | |
| public static IServiceCollection AddOpenIdConnectCookieOptions(this IServiceCollection services) | ||
| { | ||
| services.AddTransient<OpenIdConnectCookieEventsForApi>(); | ||
| services.AddTransient<ITokenService, DefaultTokenService>(); | ||
| services.AddTransient<IUserTokenEndpointService, UserTokenEndpointService>(); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Trengs Tokenservice i denne lenger? Er jo flyttet opp til sampelet |
||
| services.AddTransient<ITokenService, TokenService>(); | ||
| services.AddSingleton<IPostConfigureOptions<CookieAuthenticationOptions>, OpenIdConnectCookieAuthenticationOptions>(); | ||
|
|
||
| return services; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| using Duende.IdentityModel; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. La oss ta en gjennomgang på denne. Jeg har noen tanker her. |
||
| using Duende.IdentityModel.Client; | ||
| using Fhi.Authentication.Tokens; | ||
|
|
||
| namespace Fhi.Authentication.Setup; | ||
|
|
||
| public static class HttpClientExtensions | ||
| { | ||
| public static async Task<TokenResponse> RequestTokenWithDPoP( | ||
| this HttpClient client, | ||
| DiscoveryDocumentResponse discovery, | ||
| string clientId, | ||
| string jwk, | ||
| string scopes, | ||
| string dPopJwk, | ||
| string? nonce = null) | ||
| { | ||
| var tokenRequest = new ClientCredentialsTokenRequest | ||
| { | ||
| ClientId = clientId, | ||
| Address = discovery.TokenEndpoint, | ||
| GrantType = OidcConstants.GrantTypes.ClientCredentials, | ||
| ClientCredentialStyle = ClientCredentialStyle.PostBody, | ||
| DPoPProofToken = DPoPProofGenerator.CreateDPoPProof( | ||
| discovery.TokenEndpoint!, | ||
| "POST", | ||
| dPopJwk, | ||
| "PS256", | ||
| dPoPNonce: nonce), | ||
| ClientAssertion = new ClientAssertion | ||
| { | ||
| Type = OidcConstants.ClientAssertionTypes.JwtBearer, | ||
| Value = ClientAssertionTokenHandler.CreateJwtToken(discovery.Issuer!, clientId, jwk) | ||
| }, | ||
| Scope = scopes | ||
| }; | ||
|
|
||
| return await client.RequestClientCredentialsTokenAsync(tokenRequest); | ||
| } | ||
| } | ||
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| using System.IdentityModel.Tokens.Jwt; | ||
| using System.Security.Cryptography; | ||
| using System.Text; | ||
| using Duende.IdentityModel; | ||
| using Microsoft.IdentityModel.Tokens; | ||
|
|
||
| namespace Fhi.Authentication.Tokens | ||
| { | ||
| // TODO: Need to go through this. Copied from HelseId samples and Duende. Should we handle multiple algorithms? | ||
| // need to figure out what algs, size etc. to support | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Vi må se litt mer på denne. Mulig ny oppgave. Men skal den være sånn så bør vi la den være private. |
||
|
|
||
| public static class DPoPProofGenerator | ||
| { | ||
| /// <summary> | ||
| /// | ||
| /// </summary> | ||
| /// <param name="url"></param> | ||
| /// <param name="httpMethod"></param> | ||
| /// <param name="key">public and private key</param> | ||
| /// <param name="keyAlgorithm"></param> | ||
| /// <param name="dPoPNonce"></param> | ||
| /// <param name="accessToken"></param> | ||
| /// <returns></returns> | ||
| /// <exception cref="InvalidOperationException"></exception> | ||
| public static string CreateDPoPProof(string url, string httpMethod, string key, string keyAlgorithm, string? dPoPNonce = null, string? accessToken = null) | ||
| { | ||
| var securityKey = new JsonWebKey(key); | ||
| var signingCredentials = new SigningCredentials(securityKey, keyAlgorithm); | ||
|
|
||
| var jwk = securityKey.Kty switch | ||
| { | ||
| JsonWebAlgorithmsKeyTypes.EllipticCurve => new Dictionary<string, string> | ||
| { | ||
| [JsonWebKeyParameterNames.Kty] = securityKey.Kty, | ||
| [JsonWebKeyParameterNames.X] = securityKey.X, | ||
| [JsonWebKeyParameterNames.Y] = securityKey.Y, | ||
| [JsonWebKeyParameterNames.Crv] = securityKey.Crv, | ||
| }, | ||
| JsonWebAlgorithmsKeyTypes.RSA => new Dictionary<string, string> | ||
| { | ||
| [JsonWebKeyParameterNames.Kty] = securityKey.Kty, | ||
| [JsonWebKeyParameterNames.N] = securityKey.N, | ||
| [JsonWebKeyParameterNames.E] = securityKey.E, | ||
| }, | ||
| _ => throw new InvalidOperationException("Invalid key type for DPoP proof.") | ||
| }; | ||
|
|
||
| var jwtHeader = new JwtHeader(signingCredentials) | ||
| { | ||
| [JwtClaimTypes.TokenType] = "dpop+jwt", | ||
| [JwtClaimTypes.JsonWebKey] = jwk, | ||
| }; | ||
|
|
||
| var payload = new JwtPayload | ||
| { | ||
| [JwtClaimTypes.JwtId] = Guid.NewGuid().ToString(), | ||
| [JwtClaimTypes.DPoPHttpMethod] = httpMethod, | ||
| [JwtClaimTypes.DPoPHttpUrl] = url, | ||
| [JwtClaimTypes.IssuedAt] = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), | ||
| }; | ||
|
|
||
| // Used when accessing the authentication server (HelseID): | ||
| if (!string.IsNullOrEmpty(dPoPNonce)) | ||
| { | ||
| // nonce: A recent nonce provided via the DPoP-Nonce HTTP header. | ||
| payload[JwtClaimTypes.Nonce] = dPoPNonce; | ||
| } | ||
|
|
||
| // Used when accessing an API that requires a DPoP token: | ||
| if (!string.IsNullOrEmpty(accessToken)) | ||
| { | ||
| // ath: hash of the access token. The value MUST be the result of a base64url encoding | ||
| // the SHA-256 [SHS] hash of the ASCII encoding of the associated access token's value. | ||
| using var sha256 = SHA256.Create(); | ||
| var hash = sha256.ComputeHash(Encoding.ASCII.GetBytes(accessToken)); | ||
| var ath = Base64Url.Encode(hash); | ||
|
|
||
| payload[JwtClaimTypes.DPoPAccessTokenHash] = ath; | ||
| } | ||
|
|
||
| var jwtSecurityToken = new JwtSecurityToken(jwtHeader, payload); | ||
| return new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken); | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove before merge!