Sign inSign up

barzinassa/microidp

By barzinassa

•Updated 3 months ago

Micro IDP Service - Sign up/Sign in with Email - Sign up/Sign in with Google

Image
Security
API management
0

3.5K

barzinassa/microidp repository overview

⁠MicroIDP

A lightweight Identity Provider (IDP) service built with ASP.NET Core and MongoDB. It handles user authentication and issues RSA-signed JWTs stored in encrypted HTTP-only cookies, making it easy to integrate with any client application.

⁠Features

  • Register and sign in with email and password
  • Sign in with Google OAuth 2.0
  • Email verification on registration
  • Forgot password / reset password via email
  • JWT access tokens signed with RSA key pair
  • Encrypted HTTP-only cookie storage (ASP.NET Core Data Protection)
  • Automatic token refresh
  • Cloudflare Turnstile CAPTCHA protection on sensitive endpoints
  • Swagger UI (enabled in development)

⁠Architecture

The solution follows a clean architecture pattern:

ProjectDescription
DomainEntities, interfaces, models, and enums
DataAccessMongoDB repository implementations
ServiceBusiness logic (user, JWT, email, security, Turnstile)
IoCConfigDependency injection wiring
WebApiASP.NET Core Web API (controllers, view models, email templates)
Service.testUnit tests

⁠API Endpoints

All endpoints are under /api/auth.

MethodEndpointAuthDescription
POST/login—Sign in with email and password
POST/register—Create a new account
GET/logout—Sign out and revoke tokens
GET/refresh-token—Refresh the access token from cookie
GET/userJWTGet current user info
POST/change-passwordJWTChange password
POST/forgot-password—Send a password reset email
POST/reset-password—Reset password using emailed token
POST/resend-verification-email—Resend the email verification link
GET/verify-email—Verify email with token from link
GET/google-login—Redirect to Google OAuth consent
GET/google-callback—Handle Google OAuth callback
GET/google-connectJWTConnect a Google account to the current user
GET/google-connect-callbackJWTHandle Google OAuth callback for account linking

⁠Prerequisites

⁠Run in Docker

⁠1. Generate an HTTPS Certificate
⁠Windows
dotnet dev-certs https -ep %USERPROFILE%\.aspnet\https\aspnetapp.pfx -p <CERT_PASSWORD>
dotnet dev-certs https --trust
⁠Linux
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
  -keyout aspnetcore.key -out aspnetcore.crt -subj "/CN=localhost"
openssl pkcs12 -export -out aspnetcore.pfx \
  -inkey aspnetcore.key -in aspnetcore.crt

Update the volume mount in docker-compose.yml to point to the generated .pfx file:

volumes:
  - type: bind
    source: ./aspnetcore.pfx
    target: /https/aspnetcore.pfx
⁠2. Create the Data Protection Keys Folder

This folder is shared between containers so that cookies can be encrypted/decrypted across restarts:

mkdir DataProtectionKeys

Ensure it is mounted in docker-compose.yml:

volumes:
  - type: bind
    source: ./DataProtectionKeys
    target: /app/DataProtectionKeys
⁠3. Generate the RSA Key Pair

JWTs are signed with a private key and verified by client apps using the corresponding public key.

Open and run generate-RSA.ipynb included in the repository.

⁠Option B — C# Interactive
using System.Security.Cryptography;
using (var rsa = RSA.Create(2048))
{
    var privateKey = rsa.ExportRSAPrivateKey();
    Console.WriteLine("Private Key:");
    Console.WriteLine(Convert.ToBase64String(privateKey));

    var publicKey = rsa.ExportRSAPublicKey();
    Console.WriteLine("\nPublic Key:");
    Console.WriteLine(Convert.ToBase64String(publicKey));
}
⁠Option C — Bash / OpenSSL
openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048
openssl pkey -in private_key.pem -outform DER | base64 -w 0

openssl rsa -pubout -in private_key.pem -out public_key.pem
openssl rsa -pubin -in public_key.pem -outform DER | base64 -w 0
⁠4. Create a .env File

Create a .env file in the repository root with the following variables:

# HTTPS certificate
ASPNETCORE_Kestrel__Certificates__Default__Path=/https/aspnetcore.pfx
ASPNETCORE_Kestrel__Certificates__Default__Password=<CERT_PASSWORD>

# RSA private key (base64-encoded DER)
Jwt__PrivateKey=<PRIVATE_KEY>

# JWT settings
Jwt__Issuer=https://localhost:8001
Jwt__Audience=http://localhost:5010
Jwt__AccessTokenExpirationMinutes=5
Jwt__RefreshTokenExpirationMinutes=60
Jwt__CookieName=microidp
Jwt__DataProtectionApplicationName=microidp
Jwt__DataProtectionPurpose=JwtCookieEncryption
Jwt__DataProtectionKeysPath=./DataProtectionKeys
Jwt__AllowMultipleLoginsFromTheSameUser=true
Jwt__AllowSignoutAllUserActiveClients=true

# MongoDB
ConnectionStrings__MongoDb=mongodb://microidp:microidp@mongo:27017
DbName=MicroIDP

# SMTP
SMTP__Host=<SMTP_HOST>
SMTP__Port=587
SMTP__Username=<SMTP_USERNAME>
SMTP__Password=<SMTP_PASSWORD>

# Email templates
EmailTemplate__ResetPasswordUrl=https://localhost:8001/reset-password
EmailTemplate__EmailVerificationUrl=https://localhost:8001/verify-email
EmailTemplate__ApplicationName=MicroIDP

# Google OAuth
OAuth__GoogleClientId=<GOOGLE_CLIENT_ID>
OAuth__GoogleClientSecret=<GOOGLE_CLIENT_SECRET>
OAuth__GoogleCallbackURL=<CLIENT_APP_GOOGLE_CALLBACK_URL>
OAuth__GoogleConnectCallbackURL=<CLIENT_APP_GOOGLE_CONNECT_CALLBACK_URL>

# Cloudflare Turnstile
Turnstile__SecretKey=<TURNSTILE_SECRET_KEY>

# Data Protection
DataProtection__GeneralPurposeKey=<RANDOM_SECRET_STRING>
⁠5. Configure Google Sign-In
  1. Create an OAuth 2.0 client ID in Google Cloud Console⁠.
  2. Add https://localhost:8001/api/auth/google-callback and https://localhost:8001/api/auth/google-connect-callback (or your production URLs) as authorised redirect URIs.
  3. Set OAuth__GoogleClientId and OAuth__GoogleClientSecret in .env.
  4. Set OAuth__GoogleCallbackURL to the page in your client app that Google redirects to after a login. That page should then call GET https://IDP_SERVER_URL/api/auth/google-callback to receive the JWT cookie.
  5. Set OAuth__GoogleConnectCallbackURL to the page in your client app that Google redirects to after an account-linking flow (GET /api/auth/google-connect). That page should call GET https://IDP_SERVER_URL/api/auth/google-connect-callback.
⁠6. Start the Service
docker compose up --wait

The API will be available at https://localhost:8001. Swagger UI is served at https://localhost:8001/swagger when EnableSwagger is true.

⁠Integrating with a Client App

Client apps validate JWTs using only the public key. The access token is never exposed to JavaScript — it lives inside an encrypted, HTTP-only, Secure, SameSite=Strict cookie managed by MicroIDP.

⁠1. Add Jwt Section to appsettings.json
"Jwt": {
    "PublicKey": "<PUBLIC_KEY>",
    "Issuer": "https://localhost:8001",
    "Audience": "http://localhost:5010",
    "DataProtectionApplicationName": "microidp",
    "DataProtectionKeysPath": "./DataProtectionKeys",
    "CookieName": "microidp",
    "DataProtectionPurpose": "JwtCookieEncryption"
}

CookieName and DataProtectionPurpose must match the values used by MicroIDP.

⁠2. Install Required Packages
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
dotnet add package Microsoft.AspNetCore.DataProtection
⁠3. Register Authentication Middleware
services.AddDataProtection()
    .PersistKeysToFileSystem(new DirectoryInfo(configuration["Jwt:DataProtectionKeysPath"] ?? ""))
    .SetApplicationName(configuration["Jwt:DataProtectionApplicationName"] ?? "");

var rsa = RSA.Create();
rsa.ImportRSAPublicKey(Convert.FromBase64String(configuration["Jwt:PublicKey"] ?? ""), out _);

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = configuration["Jwt:Issuer"],
            ValidAudience = configuration["Jwt:Audience"],
            IssuerSigningKey = new RsaSecurityKey(rsa)
        };
        options.Events = new JwtBearerEvents
        {
            OnMessageReceived = context =>
            {
                if (context.Request.Cookies.TryGetValue(configuration["Jwt:CookieName"] ?? "", out var encryptedToken))
                {
                    var dataProtector = context.HttpContext.RequestServices
                        .GetRequiredService<IDataProtectionProvider>()
                        .CreateProtector(configuration["Jwt:DataProtectionPurpose"] ?? "");

                    try
                    {
                        var authCookie = JsonSerializer.Deserialize<AuthCookie>(
                            dataProtector.Unprotect(encryptedToken));
                        context.Token = authCookie?.AccessToken;
                    }
                    catch
                    {
                        context.Fail("Invalid or tampered token");
                    }
                }

                return Task.CompletedTask;
            }
        };
    });

The DataProtectionKeys folder must be the same shared folder mounted into the MicroIDP container so that keys are identical on both sides.

Tag summary

Content type

Image

Digest

sha256:f9ab08447…

Size

55.8 MB

Last updated

3 months ago

docker pull barzinassa/microidp