A lightweight OAuth2/OpenID Connect identity provider supporting multiple grant types and hardcoded users, designed for development and testing scenarios.
[TOC]
- OAuth2 and OpenID Connect support
- Public and confidential client support
- Multiple grant types (authorization code, password, client credentials, refresh token)
- JWT token generation
- Debug endpoints for development
- No database required
- Built-in user selection interface
- CORS support
- Client redirect URI validation
- Support for SPA, server, and API tool clients
- Cookie-based Single Sign-On (SSO) across browser tabs
<PackageReference Include="BridgingIT.DevKit.Presentation.Web" Version="x.y.z" />builder.Services.AddFakeIdentityProvider(options => {
options.Enabled(builder.Environment.IsDevelopment())
.WithIssuer("https://localhost:5001")
.WithUsers(Fakes.Users)
.WithTokenLifetimes(
accessToken: TimeSpan.FromMinutes(30),
refreshToken: TimeSpan.FromDays(1))
.EnableCookieSingleSignOn() // Optional; enabled by default
.EnablePersistentRefreshTokens() // Required for cookie SSO; enabled by default
// Public Client (SPA)
.WithClient(
"spa-client",
"Angular SPA",
"http://localhost:4200/callback")
// Blazor WASM (Public Client)
.WithClient(
"blazor-wasm",
"Blazor WASM Frontend",
"https://localhost:5001/authentication/login-callback",
"https://localhost:5001/authentication/logout-callback")
// Confidential Client (Server)
.WithConfidentalClient(
"mvc-app",
"MVC Server",
"mvc-secret",
["https://localhost:5002/signin-oidc"])
// Blazor Server (Confidential Client)
.WithConfidentalClient(
"blazor-server",
"Blazor Server Frontend",
"server-secret",
["https://localhost:5003/signin-oidc"])
// WebAPI Backend
.WithConfidentalClient(
"api-backend",
"API Backend",
"api-secret",
["https://localhost:5001"])
// API Tools
.WithClient(
"swagger",
"Swagger UI",
"https://localhost:5001/swagger/oauth2-redirect.html");
});public static class Fakes
{
public static readonly FakeUser[] Users = [
new("luke.skywalker@starwars.com", "Luke Skywalker",
[Role.Administrators, Role.Users], isDefault: true),
new("yoda@starwars.com", "Yoda",
[Role.Administrators])
// ...... Add more users
];
}JSON Web Token (JWT) containing:
- User identity (sub, email)
- User info (name, roles)
- Token metadata (iss, aud, exp)
- Scope permissions
OpenID Connect token with:
- Required claims (iss, sub, aud, exp)
- Profile data (name, email)
- Role information
Long-lived token for obtaining new access tokens.
sequenceDiagram
participant Client
participant IDP
participant User
Client->>IDP: GET /authorize
alt User has auth cookie (SSO)
IDP->>Client: Code (no login page)
else First visit or cookie expired
IDP->>User: Show login page
User->>IDP: Select user
IDP->>Client: Code + Set auth cookie
end
Client->>IDP: POST /token
Note over Client,IDP: Exchange code for tokens
IDP->>Client: Access + Refresh tokens
Client->>IDP: GET /userinfo
IDP->>Client: User data
Key points:
- No client secret
- State parameter required
- Access token in Authorization header
- Refresh token for token renewal
sequenceDiagram
participant Client
participant IDP
participant User
Client->>IDP: GET /authorize
alt User has auth cookie (SSO)
IDP->>Client: Code (no login page)
else First visit or cookie expired
IDP->>User: Show login page
User->>IDP: Select user
IDP->>Client: Code + Set auth cookie
end
Client->>IDP: POST /token
Note over Client,IDP: With client_secret
IDP->>Client: Access + Refresh tokens
Client->>IDP: GET /userinfo
IDP->>Client: User data
Key points:
- Client secret required
- Secure token storage
- Server-side token management
- Valid redirect URIs enforced
When enabled (default), the fake identity provider sets an HTTP-only authentication cookie during the first authorization code flow. On subsequent /authorize requests, the provider checks this cookie and immediately redirects with a new authorization code if the user is still authenticated, skipping the user selection page.
sequenceDiagram
participant Tab1 as Browser Tab 1
participant Tab2 as Browser Tab 2
participant IDP as Identity Provider
Tab1->>IDP: GET /authorize
IDP->>Tab1: Show login page
Tab1->>IDP: Select user
IDP->>Tab1: Code + Set auth cookie
Tab1->>IDP: POST /token (exchange code)
IDP->>Tab1: Access + Refresh tokens
Note over Tab2,IDP: Open app in new tab
Tab2->>IDP: GET /authorize (with cookie)
IDP->>IDP: Validate cookie & user
IDP->>Tab2: Code (no login page)
Tab2->>IDP: POST /token (exchange code)
IDP->>Tab2: Access + Refresh tokens
| Option | Default | Description |
|---|---|---|
EnableCookieSingleSignOn |
true |
When true, the authorize endpoint checks for an existing auth cookie and skips the login page for already-authenticated users. |
EnablePersistentRefreshTokens |
true |
Must be true for cookie SSO to work; this sets the auth cookie during token exchange. |
To disable cookie SSO and always show the login page:
builder.Services.AddFakeIdentityProvider(options =>
{
options.Enabled(builder.Environment.IsDevelopment())
.EnableCookieSingleSignOn(false)
.WithUsers(Fakes.Users)
// ...
});- The authorize endpoint still validates
client_idandredirect_uribefore the SSO redirect, even when a valid cookie is present. - The cookie is HTTP-only (
options.Cookie.HttpOnly = true) and Secure (options.Cookie.SecurePolicy = CookieSecurePolicy.Always). - The cookie inherits the refresh token lifetime (default 1 day).
import { AuthConfig } from 'angular-oauth2-oidc';
export const authConfig: AuthConfig = {
issuer: 'http://localhost:5000',
redirectUri: window.location.origin + '/callback',
clientId: 'spa-client',
scope: 'openid profile email roles'
};builder.Services.AddAuthentication(options => {
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options => {
options.Authority = "http://localhost:5000";
options.ClientId = "mvc-app";
options.ClientSecret = "mvc-secret";
options.ResponseType = "code";
options.SaveTokens = true;
});// Client Program.cs
builder.Services.AddOidcAuthentication(options =>
{
options.ProviderOptions.Authority = "https://localhost:5001";
options.ProviderOptions.ClientId = "blazor-wasm";
options.ProviderOptions.DefaultScopes.Add("roles");
options.ProviderOptions.ResponseType = "code";
options.ProviderOptions.PostLogoutRedirectUri = "authentication/logout-callback";
options.ProviderOptions.RedirectUri = "authentication/login-callback";
});
// Client App.razor
<CascadingAuthenticationState>
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
<NotAuthorized>
<RedirectToLogin />
</NotAuthorized>
</AuthorizeRouteView>
</Found>
</Router>
</CascadingAuthenticationState>// Program.cs
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "https://localhost:5001";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = true,
ValidAudience = "api",
ValidateIssuer = true,
ValidIssuer = "https://localhost:5001"
};
});
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("RequireAdminRole", policy =>
policy.RequireRole("Administrators"));
});
// WeatherForecastController.cs
[ApiController]
[Route("[controller]")]
[Authorize]
public class WeatherForecastController : ControllerBase
{
[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
var user = User.Identity.Name;
// Implementation
}
[HttpPost]
[Authorize(Policy = "RequireAdminRole")]
public IActionResult Create(WeatherForecast forecast)
{
// Implementation
}
}Start OAuth2 flow and user selection.
GET /api/_system/identity/connect/authorizeParameters:
response_type: "code"client_id: Client identifierredirect_uri: Return URLscope: Requested permissionsstate: Security token
Issue tokens using various grant types.
POST /api/_system/identity/connect/token
Content-Type: application/x-www-form-urlencodedGrant Types:
- Authorization Code
grant_type=authorization_code
&client_id=client_id
&code=auth_code
&redirect_uri=callback_url- Password Grant
grant_type=password
&client_id=client_id
&username=user@example.com
&scope=openid profile- Client Credentials
grant_type=client_credentials
&client_id=client_id
&scope=api- Refresh Token
grant_type=refresh_token
&client_id=client_id
&refresh_token=tokenResponse:
{
"access_token": "eyJhbGci...",
"expires_in": 1800,
"refresh_token": "eyJhbGci...",
"token_type": "Bearer",
"scope": "openid profile",
"id_token": "eyJhbGci..."
}Get authenticated user data.
GET /api/_system/identity/connect/userinfo
Authorization: Bearer tokenResponse:
{
"sub": "user_id",
"name": "User Name",
"email": "user@example.com",
"roles": ["Admin", "User"]
}Development information about configuration.
GET /api/_system/identity/connect/debuginfoResponse:
{
"tokenIssuer": "https://localhost:5001",
"configuredClients": [
{
"clientId": "spa-client",
"name": "SPA App",
"redirectUris": ["http://localhost:4200/callback"]
}
],
"configuredUsers": [
{
"email": "user@example.com",
"name": "User Name",
"roles": ["Admin"]
}
]
}OpenID Connect discovery document.
GET /api/_system/identity/.well-known/openid-configurationResponse:
{
"issuer": "https://localhost:5001",
"authorization_endpoint": "https://localhost:5001/api/_system/identity/connect/authorize",
"token_endpoint": "https://localhost:5001/api/_system/identity/connect/token",
"userinfo_endpoint": "https://localhost:5001/api/_system/identity/connect/userinfo",
"end_session_endpoint": "https://localhost:5001/api/_system/identity/connect/logout"
}Standard OAuth2 error responses:
{
"error": "invalid_request",
"error_description": "Error details"
}Common error codes:
- invalid_request
- invalid_client
- invalid_grant
- unauthorized_client
- unsupported_grant_type
- Designed for development and testing
- No production security measures
- Hardcoded users and clients
- No token encryption requirements
- Client secrets not validated
- Access tokens never expire by default
- No real user authentication
- CORS enabled for all origins
- Debug endpoints exposed
- Cookie SSO is enabled by default for development convenience
- The auth cookie is scoped to the IDP origin and shared across browser tabs
- Client validation (
client_id,redirect_uri) still applies to SSO redirects - For testing scenarios where each request should show the login page, disable with
EnableCookieSingleSignOn(false)
// Multiple redirect URIs
.WithClient(
"angular-app",
"Angular Frontend",
["http://localhost:4200/callback",
"http://localhost:4200/silent-refresh"])
// API documentation tools
.WithClient(
"swagger",
"Swagger UI",
"https://localhost:5001/swagger/oauth2-redirect.html")- Multiple users
- Role-based access
- Token validation
- Authentication flows
- Client registrations
- OAuth 2.0 RFC
- OpenID Connect Specification
- JWT Documentation
A lightweight OAuth2/OpenID Connect identity provider supporting multiple grant types and hardcoded users. Perfect for development and testing scenarios where a full-fledged identity provider would be overkill.
- OAuth2 and OpenID Connect support
- Multiple grant types (authorization code, password, client credentials, refresh token)
- Hardcoded user management
- JWT token generation
- No database required
- Minimal setup
<PackageReference Include="BridgingIT.DevKit.Presentation.Web" Version="1.0.0" />In your Program.cs:
builder.Services.AddFakeIdentityProvider(options =>
{
options.Users = Fakes.Users; // Your hardcoded users
options.TokenIssuer = "http://localhost:5000"; // Your IDP base URL
options.GroupPrefix = "/api/_system/identity"; // Base route for all endpoints
});public static class Fakes
{
public static readonly FakeUser[] Users =
[
new("luke.skywalker@starwars.com", "Luke Skywalker",
[Role.Administrators, Role.Users], isDefault: true),
new("yoda@starwars.com", "Yoda",
[Role.Administrators])
];
}Configure your Angular application with OIDC client library:
import { AuthConfig } from 'angular-oauth2-oidc';
export const authConfig: AuthConfig = {
issuer: 'http://localhost:5000',
redirectUri: window.location.origin + '/callback',
clientId: 'any_client_id',
scope: 'openid profile email roles',
responseType: 'code'
};sequenceDiagram
participant Browser
participant App
participant IDP
App->>Browser: 1. Redirect to /connect/authorize
Browser->>IDP: 2. GET /connect/authorize
IDP->>Browser: 3. Show user selection page
Browser->>Browser: 4. Select user
Browser->>IDP: 5. GET /connect/authorize/callback
IDP->>Browser: 6. Redirect with code
Browser->>App: 7. Pass authorization code
App->>IDP: 8. POST /connect/token
Note over App,IDP: Exchange code for tokens
IDP->>App: 9. Return tokens (access + refresh)
App->>IDP: 10. GET /connect/userinfo
Note over App,IDP: Get user data with access token
IDP->>App: 11. Return user info
POST /api/_system/identity/connect/token
Content-Type: application/x-www-form-urlencoded
grant_type=password
&client_id=any_client_id
&username=luke.skywalker@starwars.com
&scope=openid profile email rolesPOST /api/_system/identity/connect/token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
&client_id=any_client_id
&scope=apiNote: This IDP implementation does not validate client_ids. Any string can be used as client_id in the requests.
Starts the OAuth2 authorization flow with user selection.
GET /api/_system/identity/connect/authorize?
response_type=code
&client_id=any_client_id
&redirect_uri=http://localhost:4200/callback
&scope=openid profile email roles
&state=abc123Issues tokens using various grant types.
POST /api/_system/identity/connect/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&client_id=any_client_id
&code=xyz789
&redirect_uri=http://localhost:4200/callbackSample Response:
{
"access_token": "eyJhbGci...",
"expires_in": 1800,
"refresh_expires_in": 1800,
"refresh_token": "eyJhbGci...",
"token_type": "Bearer",
"scope": "openid email profile roles",
"session_state": "5adfe176-2f18-4803-a0b3-21c678c49b5b"
}Returns information about the authenticated user.
GET /api/_system/identity/connect/userinfo
Authorization: Bearer eyJhbGci...Response:
{
"sub": "749ecbc50c2364add0caa40f9afc2bbf",
"name": "Luke Skywalker",
"given_name": "Luke",
"family_name": "Skywalker",
"preferred_username": "luke.skywalker@starwars.com",
"email": "luke.skywalker@starwars.com",
"email_verified": true,
"roles": ["Administrators", "Users"]
}Returns the OpenID Connect discovery document.
GET /api/_system/identity/.well-known/openid-configurationResponse:
{
"issuer": "http://localhost:5000",
"authorization_endpoint": "http://localhost:5000/api/_system/identity/connect/authorize",
"token_endpoint": "http://localhost:5000/api/_system/identity/connect/token",
"userinfo_endpoint": "http://localhost:5000/api/_system/identity/connect/userinfo",
"end_session_endpoint": "http://localhost:5000/api/_system/identity/connect/logout",
"grant_types_supported": [
"authorization_code",
"password",
"client_credentials",
"refresh_token"
],
"response_types_supported": ["code"],
"scopes_supported": ["openid", "profile", "email", "roles"],
"claims_supported": [
"sub",
"name",
"family_name",
"given_name",
"preferred_username",
"email",
"email_verified"
]
}Handles user logout with optional redirect.
POST /api/_system/identity/connect/logout?
post_logout_redirect_uri=http://localhost:4200
&state=abc123All endpoints return OAuth2 compliant error responses:
{
"error": "invalid_request",
"error_description": "The request is missing a required parameter"
}Common error codes:
- invalid_request
- invalid_client
- invalid_grant
- unauthorized_client
- unsupported_grant_type
- invalid_scope
This Identity Provider is designed exclusively for development and testing environments, with intentionally simplified security measures that make it unsuitable for production use. It provides basic OAuth2 and OpenID Connect flows without rigorous security validation. User credentials are stored in plaintext, authentication is simplified, and token validation is minimal. Security features like rate limiting, audit logging, and proper session management are omitted for development convenience.
The provider includes development-friendly features like debug endpoints and permissive CORS policies that would pose security risks in production. Use this provider only in controlled development environments, ideally on localhost or protected development networks. Never expose it to public networks, use it with production data, or connect it to production services. For production deployments, always use a properly secured identity provider.
- This IDP is designed for development and testing
- Uses symmetric key signing (HS256)
- No real user authentication
- No real client secret validation