Skip to content

Commit c968bbe

Browse files
authored
Merge pull request #17 from vinder-io/feature/VI-2-implements-authorization-code-grant
implements authorization code grant
2 parents 8bf3faa + 02ef382 commit c968bbe

47 files changed

Lines changed: 808 additions & 71 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace Vinder.Federation.Application.Contracts;
2+
3+
public interface IAuthorizationFlowHandler
4+
{
5+
public Task<Result<ClientAuthenticationResult>> HandleAsync(
6+
ClientAuthenticationCredentials parameters,
7+
CancellationToken cancellation = default
8+
);
9+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
namespace Vinder.Federation.Application.Handlers.Authorization;
2+
3+
public sealed class AuthorizationCodeGrantHandler(ITenantCollection tenantCollection, IUserCollection userCollection, ISecurityTokenService tokenService, ITokenCollection tokenCollection) : IAuthorizationFlowHandler
4+
{
5+
public async Task<Result<ClientAuthenticationResult>> HandleAsync(
6+
ClientAuthenticationCredentials parameters, CancellationToken cancellation = default)
7+
{
8+
var filters = new TokenFiltersBuilder()
9+
.WithValue(parameters.Code)
10+
.WithType(TokenType.AuthorizationCode)
11+
.Build();
12+
13+
var tokens = await tokenCollection.GetTokensAsync(filters, cancellation: cancellation);
14+
var token = tokens.FirstOrDefault();
15+
16+
if (token is null)
17+
{
18+
return Result<ClientAuthenticationResult>.Failure(AuthorizationErrors.InvalidAuthorizationCode);
19+
}
20+
21+
if (token.IsExpired)
22+
{
23+
return Result<ClientAuthenticationResult>.Failure(AuthorizationErrors.AuthorizationCodeExpired);
24+
}
25+
26+
var tenantFilters = new TenantFiltersBuilder()
27+
.WithIdentifier(token.TenantId)
28+
.Build();
29+
30+
var tenants = await tenantCollection.GetTenantsAsync(tenantFilters, cancellation: cancellation);
31+
var tenant = tenants.FirstOrDefault();
32+
33+
if (tenant is null)
34+
{
35+
return Result<ClientAuthenticationResult>.Failure(AuthenticationErrors.ClientNotFound);
36+
}
37+
38+
var codeChallenge = token.Metadata.GetValueOrDefault("code.challenge")!;
39+
var codeChallengeMethod = token.Metadata.GetValueOrDefault("code.challenge.method")!;
40+
41+
if (!PkceCodeVerifier.Validate(parameters.CodeVerifier, codeChallenge, codeChallengeMethod))
42+
{
43+
return Result<ClientAuthenticationResult>.Failure(AuthorizationErrors.InvalidCodeVerifier);
44+
}
45+
46+
var userFilters = new UserFiltersBuilder()
47+
.WithIdentifier(token.UserId)
48+
.Build();
49+
50+
var users = await userCollection.GetUsersAsync(userFilters, cancellation: cancellation);
51+
var user = users.FirstOrDefault();
52+
53+
if (user is null)
54+
{
55+
return Result<ClientAuthenticationResult>.Failure(AuthenticationErrors.UserNotFound);
56+
}
57+
58+
var tokenResult = await tokenService.GenerateAccessTokenAsync(user, cancellation);
59+
if (tokenResult.IsFailure || tokenResult.Data is null)
60+
{
61+
return Result<ClientAuthenticationResult>.Failure(tokenResult.Error);
62+
}
63+
64+
return Result<ClientAuthenticationResult>.Success(new()
65+
{
66+
AccessToken = tokenResult.Data.Value
67+
});
68+
}
69+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
namespace Vinder.Federation.Application.Handlers.Authorization;
2+
3+
public sealed class AuthorizationHandler(ITenantCollection tenantCollection, IRedirectUriPolicy redirectUriPolicy) :
4+
IMessageHandler<AuthorizationParameters, Result<AuthorizationScheme>>
5+
{
6+
public async Task<Result<AuthorizationScheme>> HandleAsync(
7+
AuthorizationParameters parameters, CancellationToken cancellation = default)
8+
{
9+
var filters = new TenantFiltersBuilder()
10+
.WithClientId(parameters.ClientId)
11+
.Build();
12+
13+
var clients = await tenantCollection.GetTenantsAsync(filters, cancellation);
14+
var client = clients.FirstOrDefault();
15+
16+
if (client is null)
17+
{
18+
return Result<AuthorizationScheme>.Failure(TenantErrors.TenantDoesNotExist);
19+
}
20+
21+
var redirectUri = parameters.RedirectUri.AsUri();
22+
23+
// according to oauth 2.0 spec (RFC 6749, section 3.1.2.3):
24+
// https://datatracker.ietf.org/doc/html/rfc6749#section-3.1.2.3
25+
26+
var redirectProof = await redirectUriPolicy.EnsureRedirectUriIsAllowedAsync(client, redirectUri, cancellation);
27+
if (redirectProof.IsFailure)
28+
{
29+
return Result<AuthorizationScheme>.Failure(redirectProof.Error);
30+
}
31+
32+
return Result<AuthorizationScheme>.Success(parameters.AsReponse());
33+
}
34+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
namespace Vinder.Federation.Application.Handlers.Authorization;
2+
3+
public sealed class ClientCredentialsGrantHandler(ITenantCollection tenantCollection, ISecurityTokenService tokenService) : IAuthorizationFlowHandler
4+
{
5+
public async Task<Result<ClientAuthenticationResult>> HandleAsync(ClientAuthenticationCredentials parameters, CancellationToken cancellation = default)
6+
{
7+
var filters = new TenantFiltersBuilder()
8+
.WithClientId(parameters.ClientId)
9+
.Build();
10+
11+
var tenants = await tenantCollection.GetTenantsAsync(filters, cancellation: cancellation);
12+
var tenant = tenants.FirstOrDefault();
13+
14+
if (tenant is null)
15+
{
16+
return Result<ClientAuthenticationResult>.Failure(AuthenticationErrors.ClientNotFound);
17+
}
18+
19+
if (parameters.ClientSecret != tenant.SecretHash)
20+
{
21+
return Result<ClientAuthenticationResult>.Failure(AuthenticationErrors.InvalidClientCredentials);
22+
}
23+
24+
var tokenResult = await tokenService.GenerateAccessTokenAsync(tenant, cancellation);
25+
if (tokenResult.IsFailure)
26+
{
27+
return Result<ClientAuthenticationResult>.Failure(tokenResult.Error);
28+
}
29+
30+
var response = new ClientAuthenticationResult
31+
{
32+
AccessToken = tokenResult.Data!.Value
33+
};
34+
35+
return Result<ClientAuthenticationResult>.Success(response);
36+
}
37+
}
Lines changed: 7 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,17 @@
11
namespace Vinder.Federation.Application.Handlers.Identity;
22

3-
public sealed class ClientAuthenticationHandler(
4-
ITenantCollection tenantCollection,
5-
ISecurityTokenService tokenService
6-
) : IMessageHandler<ClientAuthenticationCredentials, Result<ClientAuthenticationResult>>
3+
public sealed class ClientAuthenticationHandler(ITenantCollection tenantCollection, IUserCollection userCollection, ITokenCollection tokenCollection, ISecurityTokenService tokenService) :
4+
IMessageHandler<ClientAuthenticationCredentials, Result<ClientAuthenticationResult>>
75
{
86
public async Task<Result<ClientAuthenticationResult>> HandleAsync(
97
ClientAuthenticationCredentials parameters, CancellationToken cancellation = default)
108
{
11-
var filters = TenantFilters.WithSpecifications()
12-
.WithClientId(parameters.ClientId)
13-
.Build();
14-
15-
var tenants = await tenantCollection.GetTenantsAsync(filters, cancellation: cancellation);
16-
var tenant = tenants.FirstOrDefault();
17-
18-
if (tenant is null)
19-
{
20-
return Result<ClientAuthenticationResult>.Failure(AuthenticationErrors.ClientNotFound);
21-
}
22-
23-
if (parameters.ClientSecret != tenant.SecretHash)
24-
{
25-
return Result<ClientAuthenticationResult>.Failure(AuthenticationErrors.InvalidClientCredentials);
26-
}
27-
28-
var tokenResult = await tokenService.GenerateAccessTokenAsync(tenant, cancellation);
29-
if (tokenResult.IsFailure)
30-
{
31-
return Result<ClientAuthenticationResult>.Failure(tokenResult.Error);
32-
}
33-
34-
var response = new ClientAuthenticationResult
9+
IAuthorizationFlowHandler handler = parameters.GrantType switch
3510
{
36-
AccessToken = tokenResult.Data!.Value
11+
SupportedGrantType.AuthorizationCode => new AuthorizationCodeGrantHandler(tenantCollection, userCollection, tokenService, tokenCollection),
12+
SupportedGrantType.ClientCredentials => new ClientCredentialsGrantHandler(tenantCollection, tokenService),
3713
};
3814

39-
return Result<ClientAuthenticationResult>.Success(response);
15+
return await handler.HandleAsync(parameters, cancellation);
4016
}
41-
}
17+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
namespace Vinder.Federation.Application.Mappers;
2+
3+
public static class AuthorizationMapper
4+
{
5+
public static AuthorizationScheme AsReponse(this AuthorizationParameters parameters) => new()
6+
{
7+
ClientId = parameters.ClientId,
8+
RedirectUri = parameters.RedirectUri,
9+
State = parameters.State,
10+
CodeChallenge = parameters.CodeChallenge,
11+
CodeChallengeMethod = parameters.CodeChallengeMethod,
12+
};
13+
}

Source/Vinder.Federation.Application/Mappers/JsonWebKeysMapper.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
using Microsoft.IdentityModel.Tokens;
2-
using Vinder.Federation.Application.Payloads.Connect;
3-
41
namespace Vinder.Federation.Application.Mappers;
52

63
public static class JsonWebKeysMapper
@@ -25,4 +22,4 @@ public static JsonWebKeySetScheme AsJsonWebKeySetScheme(Secret secret)
2522
Keys = [JsonWebKeysMapper.AsJsonWebKeys(secret)]
2623
};
2724
}
28-
}
25+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace Vinder.Federation.Application.Mappers;
2+
3+
public static class RedirectUriMapper
4+
{
5+
public static RedirectUri AsUri(this string uri) =>
6+
new(uri);
7+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
namespace Vinder.Federation.Application.Payloads.Authorization;
2+
3+
public sealed record AuthorizationParameters : IMessage<Result<AuthorizationScheme>>
4+
{
5+
public string ResponseType { get; init; } = default!;
6+
public string RedirectUri { get; init; } = default!;
7+
8+
public string ClientId { get; init; } = default!;
9+
10+
public string CodeChallenge { get; init; } = default!;
11+
public string CodeChallengeMethod { get; init; } = default!;
12+
13+
public string? Scope { get; init; } = default!;
14+
public string? State { get; init; } = default!;
15+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
namespace Vinder.Federation.Application.Payloads.Authorization;
2+
3+
public sealed record AuthorizationScheme
4+
{
5+
public string ClientId { get; init; } = default!;
6+
public string RedirectUri { get; init; } = default!;
7+
public string CodeChallenge { get; init; } = default!;
8+
public string CodeChallengeMethod { get; init; } = default!;
9+
public string? State { get; init; } = default;
10+
}

0 commit comments

Comments
 (0)