Serverless OAuth2 server with OpenIddict 5 and AWS DynamoDB - Part 0
After succesfully running ASP.NET 8 Minimal API with Lambda Container image it's time to run something real.
And just in December 2023 Kévin Chalet announced new version of OpenIddict, the library to build your own OAuth2 / OpenID Connect server in .NET.
Officially OpenIddict supports two implementations for persistance layer:
- Relation databases using EntityFramework Core
- NoSQL with MongoDb
This time we will explore how to implement fully serverless OAuth2 server using OpenIddict 5 with Lambda Container image and persistance layer backed by AWS DynamoDB
Due to the large scope this will be series of posts covering the following aspects:
- OpenIddict custom stores implementation with DynamoDB
- Fully serverless OAuth2 server sample and setup for local testing
- CDK custom component lib for OpenIddict
- Cost analysis and comparison with Cognito, Auth0, etc
You can find source code available at https://github.com/ahanoff/OpenIddict.DynamoDb
First let's review OpenIddict concepts and components.
OpenIddict concepts
OpenIddict focuses on standard-compliant OAuth 2.0/OpenID Connect specifications.
Since OpenID Connect built on top of OAuth2, most of the concepts are overlapping, allowing OpenIddict keep simple implementation with just 4 models.
Application
Application model stores information about client application
https://datatracker.ietf.org/doc/html/rfc6749#section-2
In OAuth2 / OpenID Connect flows user delegates permission to act on user behalf to client application
.
This client application
can be browser-based app, mobile app, server-side apps, or connected devices.
Scope
Scopes are an important concept in OAuth 2.0. They are used to specify exactly the reason for which access to resources may be granted. Acceptable scope values, and which resources they relate to, are dependent on the Resource Server.
https://datatracker.ietf.org/doc/html/rfc6749#section-3.3
The authorization and token endpoints allow the client to specify the scope of the access request using the "scope" request parameter. In turn, the authorization server uses the "scope" response parameter to inform the client of the scope of the access token issued.
The value of the scope parameter is expressed as a list of space- delimited, case-sensitive strings. The strings are defined by the authorization server. If the value contains multiple space-delimited strings, their order does not matter, and each string adds an additional access range to the requested scope.
Authorization
Model that stores Authorization Request
parameters and returned Authorization Response
data
- https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1 - The client constructs the request URI by adding the following parameters to the query component of the authorization endpoint URI using the "application/x-www-form-urlencoded" format
- https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2 - If the resource owner grants the access request, the authorization server issues an authorization code and delivers it to the client by adding the following parameters to the query component of the redirection URI using the "application/x-www-form-urlencoded" format
Token
Token model stores data for different types of token depending on the flow used:
OpenIddict custom stores
There is actually not much information (#openiddict-core/issues/574) on how to implement custom store.
But upon looking to EntityFramework Core / Mongo DB stores, it becomes clear that custom store have to implement interfaces from OpenIddict.Abstractions
package.
OpenIddict.Abstractions package
There are 4 interfaces, one for each model that we described:
- IOpenIddictApplicationStore
- IOpenIddictAuthorizationStore
- IOpenIddictScopeStore
- IOpenIddictTokenStore
Project convention
If we look at official implementations again, we can notice the following convention:
- there are two projects: one for models, and one for stores implementations
- project names are using the following format:
OpenIddict.xxx
andOpenIddict.xxx.Models
respectively
We will follow same convention for DynamoDB:
- OpenIddict.DynamoDb.Models: contains models to work with DynamoDB
- OpenIddict.DynamoDb: contains main logic to work as persistence layer for OpenIddict
Custom store models for DynamoDB
Next we create models that will be used to store OpenIddict data in DynamoDB:
This is still early development stage, thus models below are subject to change in the next posts
- OpenIddictDynamoDbApplication
- OpenIddictDynamoDbAuthorization
- OpenIddictDynamoDbScope
- OpenIddictDynamoDbToken
using System.Collections.Immutable;
namespace OpenIddict.DynamoDb.Models;
/// <summary>
/// Represents a OpenIddict application
/// </summary>
public class OpenIddictDynamoDbApplication
{
public required string Id { get; set; }
public required string ClientId { get; set; }
public string? ClientSecret { get; set; }
public string? ConcurrencyToken { get; set; }
public string? ConsentType { get; set; }
public string? DisplayName { get; set; }
public IReadOnlyList<string> DisplayNames { get; set; } = ImmutableList<string>.Empty;
public IReadOnlyList<string> Permissions { get; set; } = ImmutableList<string>.Empty;
public IReadOnlyList<string> PostLogoutRedirectUris { get; set; } = ImmutableList<string>.Empty;
public string? Properties { get; set; }
public IReadOnlyList<string> RedirectUris { get; set; } = ImmutableList<string>.Empty;
public IReadOnlyList<string> Requirements { get; set; } = ImmutableList<string>.Empty;
public string? ClientType { get; set; }
public string? ApplicationType { get; set; }
public string? JsonWebKeySet { get; set; }
public IReadOnlyDictionary<string, string>? Settings { get; set; }
}
using System.Collections.Immutable;
namespace OpenIddict.DynamoDb.Models;
/// <summary>
/// Represents a OpenIddict authorization
/// </summary>
public class OpenIddictDynamoDbAuthorization
{
public virtual required string ApplicationId { get; set; }
public virtual string? ConcurrencyToken { get; set; } = Guid.NewGuid().ToString();
public virtual DateTime? CreationDate { get; set; }
public virtual required string Id { get; set; }
public virtual string? Properties { get; set; }
public virtual IReadOnlyList<string>? Scopes { get; set; } = ImmutableList.Create<string>();
public virtual string? Status { get; set; }
public virtual string? Subject { get; set; }
public virtual string? Type { get; set; }
}
namespace OpenIddict.DynamoDb.Models;
/// <summary>
/// Represents a OpenIddict scope
/// </summary>
public class OpenIddictDynamoDbScope
{
public required string Id { get; set; }
public string? ConcurrencyToken { get; set; }
public string? Description { get; set; }
public string? Descriptions { get; set; }
public string? DisplayName { get; set; }
public string? DisplayNames { get; set; }
public required string Name { get; set; }
public string? Properties { get; set; }
public string? Resources { get; set; }
}
namespace OpenIddict.DynamoDb.Models;
/// <summary>
/// Represents a OpenIddict token
/// </summary>
public class OpenIddictDynamoDbToken
{
public virtual required string ApplicationId { get; set; }
public virtual required string AuthorizationId { get; set; }
public virtual string? ConcurrencyToken { get; set; } = Guid.NewGuid().ToString();
public virtual DateTime? CreationDate { get; set; }
public virtual DateTime? ExpirationDate { get; set; }
public virtual required string Id { get; set; }
public virtual string? Payload { get; set; }
public virtual string? Properties { get; set; }
public virtual DateTime? RedemptionDate { get; set; }
public virtual string? ReferenceId { get; set; }
public virtual string? Status { get; set; }
public virtual string? Subject { get; set; }
public virtual string? Type { get; set; }
}
Summary
Let's review what has been done so far:
- we made a short introduction on how OpenIddict works in order to correctly implement DynamoDB based custom store.
- added .NET projects based on OpenIddict convention
- added custom store models based on OpenIddict concepts
In next post we will cover full logic implementation of custom store.
You can find source code available at https://github.com/ahanoff/OpenIddict.DynamoDb/tree/main/src