barzinassa/microidp

By barzinassa

Updated 2 days ago

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

Image
Security
API management
0

1.2K

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/loginSign in with email and password
POST/registerCreate a new account
GET/logoutSign out and revoke tokens
GET/refresh-tokenRefresh the access token from cookie
GET/userJWTGet current user info
POST/change-passwordJWTChange password
POST/forgot-passwordSend a password reset email
POST/reset-passwordReset password using emailed token
POST/resend-verification-emailResend the email verification link
GET/verify-emailVerify email with token from link
GET/google-loginRedirect to Google OAuth consent
GET/google-callbackHandle 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:df371f12d

Size

55.8 MB

Last updated

2 days ago

docker pull barzinassa/microidp:v2.3.3