diff --git a/src/Server/src/AuthTokenGenerator/Abstractions/IIdentityService.cs b/src/Server/src/AuthTokenGenerator/Abstractions/IIdentityService.cs index 6281b07..d85dcc0 100644 --- a/src/Server/src/AuthTokenGenerator/Abstractions/IIdentityService.cs +++ b/src/Server/src/AuthTokenGenerator/Abstractions/IIdentityService.cs @@ -1,3 +1,4 @@ +using IdentityModel.Client; using IdOps.Models; namespace IdOps.Abstractions @@ -5,7 +6,7 @@ namespace IdOps.Abstractions public interface IIdentityService { Task RequestTokenAsync( - TokenRequestData request, + TokenRequest request, CancellationToken cancellationToken); } } diff --git a/src/Server/src/AuthTokenGenerator/Abstractions/IResultFactory.cs b/src/Server/src/AuthTokenGenerator/Abstractions/IResultFactory.cs index 9c130be..2e15859 100644 --- a/src/Server/src/AuthTokenGenerator/Abstractions/IResultFactory.cs +++ b/src/Server/src/AuthTokenGenerator/Abstractions/IResultFactory.cs @@ -2,5 +2,5 @@ public interface IResultFactory { - Task Create(TInput input, CancellationToken cancellationToken); + Task CreateRequestAsync(TInput input, CancellationToken cancellationToken); } diff --git a/src/Server/src/AuthTokenGenerator/Abstractions/ITokenClient.cs b/src/Server/src/AuthTokenGenerator/Abstractions/ITokenClient.cs index 0075406..d176cc4 100644 --- a/src/Server/src/AuthTokenGenerator/Abstractions/ITokenClient.cs +++ b/src/Server/src/AuthTokenGenerator/Abstractions/ITokenClient.cs @@ -15,4 +15,8 @@ Task RequestTokenAsync( Task RequestClientCredentialsTokenAsync( ClientCredentialsTokenRequest request, CancellationToken cancellationToken); + + Task RequestAuthorizationCodeTokenAsync( + AuthorizationCodeTokenRequest request, + CancellationToken cancellationToken); } diff --git a/src/Server/src/AuthTokenGenerator/IdentityService.cs b/src/Server/src/AuthTokenGenerator/IdentityService.cs index 76cc7d6..ed09be0 100644 --- a/src/Server/src/AuthTokenGenerator/IdentityService.cs +++ b/src/Server/src/AuthTokenGenerator/IdentityService.cs @@ -1,5 +1,3 @@ -using System.Diagnostics; -using System.IdentityModel.Tokens.Jwt; using IdentityModel.Client; using IdOps.Abstractions; using IdOps.Models; @@ -11,26 +9,23 @@ public class IdentityService : IIdentityService private readonly ITokenClient _tokenClient; private readonly ITokenAnalyzer _tokenAnalyzer; - public IdentityService( - ITokenClient tokenClient, - ITokenAnalyzer tokenAnalyzer) + public IdentityService(ITokenClient tokenClient, ITokenAnalyzer tokenAnalyzer) { _tokenClient = tokenClient; _tokenAnalyzer = tokenAnalyzer; } public async Task RequestTokenAsync( - TokenRequestData request, + TokenRequest request, CancellationToken cancellationToken) { - DiscoveryDocumentResponse disco = await _tokenClient.GetDiscoveryDocumentAsync( - request.Authority, - cancellationToken); - TokenResponse response = request.GrantType switch { - "client_credentials" => await RequestClientCredentialTokenAsync(request, disco,cancellationToken), - _ => await RequestOtherGrantTypeTokenAsync(request, disco, cancellationToken) + "client_credentials" => await _tokenClient.RequestClientCredentialsTokenAsync( + (ClientCredentialsTokenRequest)request, cancellationToken), + "authorization_code" => await _tokenClient.RequestAuthorizationCodeTokenAsync( + (AuthorizationCodeTokenRequest)request, cancellationToken), + _ => await _tokenClient.RequestTokenAsync(request, cancellationToken) }; if (response.IsError) @@ -46,7 +41,7 @@ public async Task RequestTokenAsync( return result; } - + TokenModel? accessToken = _tokenAnalyzer.Analyze(response.AccessToken); if (accessToken == null) { @@ -55,49 +50,8 @@ public async Task RequestTokenAsync( ErrorMessage = "Access Token could not be analyzed" }; } - return new RequestTokenResult(true) - { - AccessToken = accessToken - }; - } - private async Task RequestClientCredentialTokenAsync( - TokenRequestData request, - DiscoveryDocumentResponse disco, - CancellationToken cancellationToken) - { - return await _tokenClient.RequestClientCredentialsTokenAsync( - new ClientCredentialsTokenRequest - { - Address = disco.TokenEndpoint, - ClientId = request.ClientId, - ClientSecret = request.Secret, - GrantType = request.GrantType, - Scope = request.Scopes.Any() ? string.Join(" ", request.Scopes) : null - }, cancellationToken); - } - - private async Task RequestOtherGrantTypeTokenAsync( - TokenRequestData request, - DiscoveryDocumentResponse disco, - CancellationToken cancellationToken) - { - var pars = new Dictionary(); - - if (request.Scopes is { } s && s.Any()) - { - pars.Add("scope", string.Join(" ", request.Scopes)); - } - - return await _tokenClient.RequestTokenAsync( - new TokenRequest - { - Address = disco.TokenEndpoint, - ClientId = request.ClientId, - ClientSecret = request.Secret, - GrantType = request.GrantType, - Parameters = new Parameters(pars) - }, cancellationToken); + return new RequestTokenResult(true) { AccessToken = accessToken }; } } } diff --git a/src/Server/src/AuthTokenGenerator/Models/TokenClient.cs b/src/Server/src/AuthTokenGenerator/Models/TokenClient.cs index 393a97d..270f6b4 100644 --- a/src/Server/src/AuthTokenGenerator/Models/TokenClient.cs +++ b/src/Server/src/AuthTokenGenerator/Models/TokenClient.cs @@ -37,7 +37,18 @@ public async Task RequestClientCredentialsTokenAsync( CancellationToken cancellationToken) { using HttpClient httpClient = _httpClientFactory.CreateClient(); - var response = await httpClient.RequestClientCredentialsTokenAsync(request, cancellationToken); + var response = + await httpClient.RequestClientCredentialsTokenAsync(request, cancellationToken); + return response; + } + + public async Task RequestAuthorizationCodeTokenAsync( + AuthorizationCodeTokenRequest request, + CancellationToken cancellationToken) + { + using HttpClient httpClient = _httpClientFactory.CreateClient(); + var response = + await httpClient.RequestAuthorizationCodeTokenAsync(request, cancellationToken); return response; } } diff --git a/src/Server/src/AuthTokenGenerator/Models/TokenRequestData.cs b/src/Server/src/AuthTokenGenerator/Models/TokenRequestData.cs deleted file mode 100644 index d650cf0..0000000 --- a/src/Server/src/AuthTokenGenerator/Models/TokenRequestData.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace IdOps.Models; - -public record TokenRequestData( - string Authority, - string ClientId, - string Secret, - string GrantType, - IEnumerable Scopes) -{ - public bool SaveTokens { get; init; } -} diff --git a/src/Server/src/GraphQL/Client/ClientMutations.cs b/src/Server/src/GraphQL/Client/ClientMutations.cs index b450961..3ab29fc 100644 --- a/src/Server/src/GraphQL/Client/ClientMutations.cs +++ b/src/Server/src/GraphQL/Client/ClientMutations.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using HotChocolate; using HotChocolate.Types; +using IdentityModel.Client; using IdOps.Abstractions; using IdOps.Model; using IdOps.Models; @@ -64,16 +65,15 @@ public async Task RemoveClientSecretAsync( [AuthorizeClientAuthoring(AccessMode.Write, includeTenantAuth: false)] public async Task RequestTokenAsync( [Service] IIdentityService identityService, - [Service] IResultFactory requestResultFactory, + [Service] IResultFactory requestResultFactory, RequestTokenInput input, CancellationToken cancellationToken) { - var tokenRequestData = - await requestResultFactory.Create(input,cancellationToken); - - + TokenRequest tokenRequest = + await requestResultFactory.CreateRequestAsync(input,cancellationToken); + RequestTokenResult tokenResult = - await identityService.RequestTokenAsync(tokenRequestData, cancellationToken); + await identityService.RequestTokenAsync(tokenRequest, cancellationToken); return new RequestTokenPayload(tokenResult); } diff --git a/src/Server/src/GraphQL/Client/TokenRequestDataResultFactory.cs b/src/Server/src/GraphQL/Client/TokenRequestDataResultFactory.cs index bc952a9..6f28f0d 100644 --- a/src/Server/src/GraphQL/Client/TokenRequestDataResultFactory.cs +++ b/src/Server/src/GraphQL/Client/TokenRequestDataResultFactory.cs @@ -1,35 +1,60 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using IdentityModel.Client; using IdOps.Abstractions; using IdOps.Model; using IdOps.Models; +using IdOps.Security; namespace IdOps.GraphQL; -public class TokenRequestDataResultFactory : IResultFactory +public class TokenRequestDataResultFactory : IResultFactory { private readonly IEncryptionService _encryptionService; private readonly IClientService _clientService; private readonly IApiScopeService _scopeService; + private readonly IHttpClientFactory _httpClientFactory; public TokenRequestDataResultFactory( IEncryptionService encryptionService, IClientService clientService, - IApiScopeService apiScopeService) + IApiScopeService apiScopeService, + IHttpClientFactory httpClientFactory) { _encryptionService = encryptionService; _clientService = clientService; _scopeService = apiScopeService; + _httpClientFactory = httpClientFactory; } - - public async Task Create( + public async Task CreateRequestAsync( RequestTokenInput input, CancellationToken cancellationToken) { + if (input.grantType == null) + { + throw new ArgumentNullException("No grants found"); + } + + return input.grantType switch + { + "client_credentials" => await BuildClientCredentialsTokenRequestAsync(input, + cancellationToken), + _ => throw new Exception() + }; + } + + private async Task BuildClientCredentialsTokenRequestAsync( + RequestTokenInput input, + CancellationToken cancellationToken) + { + using HttpClient httpClient = _httpClientFactory.CreateClient(); + DiscoveryDocumentResponse disco = await httpClient.GetDiscoveryDocumentAsync(input.Authority, cancellationToken); + Client? client = await _clientService.GetByIdAsync(input.ClientId, cancellationToken); if (client == null) { @@ -38,35 +63,36 @@ public async Task Create( var clientId = client.ClientId; - var secretEncrypted = client.ClientSecrets.First(secret => secret.Id.Equals(input.SecretId)) - .EncryptedValue; + if (!client.AllowedGrantTypes.Contains(input.grantType)) + { + throw new ArgumentException("Grant not valid"); + } + + var secretEncrypted = client.ClientSecrets.First(secret => secret.Id.Equals(input.SecretId)).EncryptedValue; if (secretEncrypted == null) { throw new KeyNotFoundException("No encrypted secret found"); } - var secretDecrypted = - await _encryptionService.DecryptAsync(secretEncrypted, cancellationToken); + var secretDecrypted = await _encryptionService.DecryptAsync(secretEncrypted, cancellationToken); if (client.AllowedGrantTypes == null) { - throw new ArgumentNullException("No grant types found"); + throw new ArgumentNullException("Client has no registered grant types"); } - string grantType = client.AllowedGrantTypes.Contains(input.grantType) - ? input.grantType - : throw new ArgumentException("Grant not valid"); - - var scopeIds = client.AllowedScopes.ToList().ConvertAll(clientScope => clientScope.Id); + var scopeIds = client.AllowedScopes.Select(clientScope => clientScope.Id).ToList(); var scopes = await _scopeService.GetByIdsAsync(scopeIds, cancellationToken); - var scopeNames = scopes.Select(scope => scope.Name).ToList(); - - var tokenRequestData = - new TokenRequestData(input.Authority, clientId, secretDecrypted, grantType, scopeNames) - { - SaveTokens = input.SaveTokens - }; + var scopeNames = string.Join(", ", scopes.Select(scope => scope.Name)); - return tokenRequestData; + return new ClientCredentialsTokenRequest + { + Address = disco.TokenEndpoint, + ClientId = clientId, + ClientSecret = secretDecrypted, + Scope = scopeNames, + GrantType = "client_credentials" + }; } + } diff --git a/src/Server/src/GraphQL/GraphQLServiceCollectionExtensions.cs b/src/Server/src/GraphQL/GraphQLServiceCollectionExtensions.cs index 8ed2b01..e8c1e07 100644 --- a/src/Server/src/GraphQL/GraphQLServiceCollectionExtensions.cs +++ b/src/Server/src/GraphQL/GraphQLServiceCollectionExtensions.cs @@ -1,6 +1,7 @@ using System; using HotChocolate.Execution.Configuration; using HotChocolate.Types; +using IdentityModel.Client; using IdOps.Abstractions; using IdOps.Authorization; using IdOps.Data.Errors; @@ -9,7 +10,6 @@ using IdOps.GraphQL.Hashing; using IdOps.GraphQL.Publish; using IdOps.Model; -using IdOps.Models; using Microsoft.Extensions.DependencyInjection; namespace IdOps.GraphQL @@ -32,7 +32,7 @@ public static IIdOpsServerBuilder AddGraphQLServer( .AddHttpResultSerializer(); builder.Services.AddSingleton< - IResultFactory, + IResultFactory, TokenRequestDataResultFactory>(); return builder; diff --git a/src/Server/test/AuthTokenGenerator.Tests/IdentityServiceTests.cs b/src/Server/test/AuthTokenGenerator.Tests/IdentityServiceTests.cs index b5505f2..d867dde 100644 --- a/src/Server/test/AuthTokenGenerator.Tests/IdentityServiceTests.cs +++ b/src/Server/test/AuthTokenGenerator.Tests/IdentityServiceTests.cs @@ -53,7 +53,7 @@ public async Task RequestTokenAsync_WithValidRequest_ShouldReturnToken( //Act RequestTokenResult _ = - await identityService.RequestTokenAsync(CreateTokenRequestData(grantType), + await identityService.RequestTokenAsync(CreateClientCredentialsTokenRequest(grantType), CancellationToken.None); //Assert @@ -74,14 +74,16 @@ private void initializeMocks() _tokenAnalyzerMock = new Mock(MockBehavior.Strict); } - private TokenRequestData CreateTokenRequestData(string grantType) + private ClientCredentialsTokenRequest CreateClientCredentialsTokenRequest(string grantType) { - var tokenRequestData = new TokenRequestData("http://localhost:1234", - "11111111111111111111111111111111", - "thisIsATestSecret" + - "111111111111111111111111111111111111111111111111111111111111111111111", grantType, - new[] { "scope1", "scope2"}); - return tokenRequestData; + var tokenRequest = new ClientCredentialsTokenRequest(); + + tokenRequest.Address = "http://localhost:1234"; + tokenRequest.ClientId = "11111111111111111111111111111111"; + tokenRequest.ClientSecret = "thisIsATestSecret"; + tokenRequest.GrantType = grantType; + tokenRequest.Scope = "scope1, scope2"; + return tokenRequest; } private DiscoveryDocumentResponse CreateDiscoveryResponse()