Fake Identity Provider¶
A lightweight OAuth 2.0 and OpenID Connect identity provider with configured users and clients for development and testing.
- Fake Identity Provider
- Overview
- Challenges
- Solution
- Key Features
- Architecture
- Use Cases
- Basic Usage
- Detailed setup
- Token types
- Authentication flows
- Cookie single sign-on
- Client integration examples
- API reference
- Error handling
- Security considerations
- Development tips
- Related resources
- Additional request examples
Overview¶
The Fake Identity Provider supplies local OAuth 2.0 and OpenID Connect endpoints for development and automated tests. It uses configured in-memory users and clients, issues signed JWTs and provides a browser-based user selection page. It is not a production identity system.
Challenges¶
Applications that use OAuth or OpenID Connect need realistic local flows, redirect validation and token claims. Connecting every developer machine and test run to an external identity service adds setup, network and test-data dependencies. A simplistic token stub does not exercise authorization-code redirects, refresh tokens, client credentials or user-info requests.
Solution¶
AddFakeIdentityProvider(...) registers a development provider, its endpoint group, token services, cookie authentication and a named CORS policy. Fluent options configure users, public or confidential clients, issuer, endpoint paths, token lifetimes and token shape. MapEndpoints() exposes the configured endpoints with the other DevKit endpoint groups.
Key Features¶
- OAuth 2.0 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
Architecture¶
flowchart LR
A[Application startup] --> B[AddFakeIdentityProvider]
B --> C[Provider options]
B --> D[Cookie authentication]
B --> E[Token and user-info services]
B --> F[Named CORS policy]
B --> G[FakeIdentityProviderEndpoints]
H[MapEndpoints] --> G
G --> I[Authorize and callback]
G --> J[Token and refresh]
G --> K[UserInfo and discovery]
G --> L[Logout and debug information]
FakeIdentityProviderEndpoints validates authorization requests and delegates token work to IFakeIdentityProvider, ITokenService, IAuthorizationCodeService and IUserInfoService. The default provider signs JWTs locally; optional token providers adjust the token shape for Entra ID v2, Keycloak or ADFS test scenarios.
Use Cases¶
- Run authorization-code flows for SPAs, Blazor clients and server-rendered applications during local development.
- Test password, client-credentials and refresh-token requests without an external identity system.
- Exercise role and claim handling with deterministic fake users.
- Test registered redirect URIs and confidential-client secrets.
- Use browser-cookie SSO across local application tabs.
- Inspect discovery and debug endpoints while configuring a client.
Basic Usage¶
Register the provider only for Development, configure at least one user and client, add the authentication middleware and map DevKit endpoints.
using BridgingIT.DevKit.Common;
using BridgingIT.DevKit.Presentation.Web;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization();
builder.Services.AddFakeIdentityProvider(options => options
.Enabled(builder.Environment.IsDevelopment())
.WithIssuer("https://localhost:5001")
.WithUsers([
new FakeUser(
"luke.skywalker@starwars.com",
"Luke Skywalker",
["Administrators", "Users"],
password: "development-only",
isDefault: true)
])
.WithClient(
"Local SPA",
"spa-client",
"https://localhost:5001/authentication/login-callback"));
var app = builder.Build();
app.UseHttpsRedirection();
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
app.MapEndpoints();
app.Run();
After the application starts, request the discovery document and check the HTTP result before reading it:
using var client = new HttpClient();
using var response = await client.GetAsync(
"https://localhost:5001/_bdk/api/identity/connect/.well-known/openid-configuration");
if (!response.IsSuccessStatusCode)
{
Console.Error.WriteLine($"Identity discovery failed: {(int)response.StatusCode}");
return;
}
Console.WriteLine(await response.Content.ReadAsStringAsync());
A successful response contains the configured issuer and URLs for the authorize, token, user-info and logout endpoints.
Detailed setup¶
Install the package¶
<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(
"Angular SPA",
"spa-client",
"http://localhost:4200/callback")
// Blazor WebAssembly public client
.WithClient(
"Blazor WebAssembly frontend",
"blazor-wasm",
"https://localhost:5001/authentication/login-callback",
"https://localhost:5001/authentication/logout-callback")
// Confidential server client
.WithConfidentalClient(
"MVC server",
"mvc-app",
"mvc-secret",
["https://localhost:5002/signin-oidc"])
.WithConfidentalClient(
"Blazor Server frontend",
"blazor-server",
"server-secret",
["https://localhost:5003/signin-oidc"])
// Web API backend
.WithConfidentalClient(
"API backend",
"api-backend",
"api-secret",
["https://localhost:5001"])
// API documentation client
.WithClient(
"Swagger UI",
"swagger",
"https://localhost:5001/swagger/oauth2-redirect.html");
});
The WithClient and WithConfidentalClient methods take the display name first and the client ID second. WithConfidentalClient retains its current misspelling for API compatibility.
Complete the ASP.NET Core pipeline with UseCors(), UseAuthentication(), UseAuthorization() and MapEndpoints() as shown in Basic Usage.
Define users¶
public static class Fakes
{
public static readonly FakeUser[] Users = [
new("luke.skywalker@starwars.com", "Luke Skywalker",
[Role.Administrators, Role.Users],
password: "development-only",
isDefault: true),
new("yoda@starwars.com", "Yoda",
[Role.Administrators])
// ...... Add more users
];
}
Token types¶
Access token¶
JSON Web Token (JWT) containing:
- User identity (sub, email)
- User info (name, roles)
- Token metadata (iss, aud, exp)
- Scope permissions
ID token¶
OpenID Connect token with:
- Required claims (iss, sub, aud, exp)
- Profile data (name, email)
- Role information
Refresh token¶
Long-lived token for obtaining new access tokens.
Authentication flows¶
Public client flow (SPA or mobile)¶
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:
- Public clients do not send a client secret.
- Clients should send and verify
stateto correlate the authorization response. - Send the access token in the
Authorizationheader when calling user-info or protected APIs. - Use the refresh token to obtain replacement tokens.
Confidential client flow (server applications)¶
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:
- A configured confidential client must send its client secret when exchanging an authorization code or using client credentials.
- Keep tokens and the client secret on the server.
- The authorize and callback endpoints validate the redirect URI against the configured client.
Cookie single sign-on¶
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.
How it works¶
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
Configuration¶
| 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 |
Controls cookie sign-in during authorization-code and refresh-token exchange. Keep it enabled when using cookie SSO. |
Opting out¶
To disable cookie SSO and always show the login page:
builder.Services.AddFakeIdentityProvider(options =>
{
options.Enabled(builder.Environment.IsDevelopment())
.EnableCookieSingleSignOn(false)
.WithUsers(Fakes.Users)
// ...
});
Security notes¶
- 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 uses the refresh-token lifetime, which defaults to 7 days.
Client integration examples¶
Angular public client¶
import { AuthConfig } from 'angular-oauth2-oidc';
export const authConfig: AuthConfig = {
issuer: 'https://localhost:5001',
redirectUri: window.location.origin + '/callback',
clientId: 'spa-client',
scope: 'openid profile email roles'
};
ASP.NET Core MVC confidential client¶
builder.Services.AddAuthentication(options => {
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options => {
options.Authority = "https://localhost:5001";
options.ClientId = "mvc-app";
options.ClientSecret = "mvc-secret";
options.ResponseType = "code";
options.SaveTokens = true;
});
Blazor WebAssembly¶
// 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>
Web API backend¶
// Program.cs
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "https://localhost:5001";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = true,
ValidAudience = "api-backend",
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
}
}
API reference¶
Authorization endpoint¶
Start the OAuth 2.0 authorization flow and user selection.
GET /_bdk/api/identity/connect/authorize
Parameters:
response_type: "code"client_id: Client identifierredirect_uri: Return URLscope: Requested permissionsstate: Client-generated correlation value that the provider returns unchangednonce: Optional value included in the ID token
Token endpoint¶
Issue tokens using various grant types.
POST /_bdk/api/identity/connect/token
Content-Type: application/x-www-form-urlencoded
Grant types:
- Authorization code
grant_type=authorization_code
&client_id=client_id
&client_secret=secret_for_confidential_clients
&code=auth_code
&redirect_uri=callback_url
- Password Grant
grant_type=password
&client_id=client_id
&username=user@example.com
&password=configured_development_password
&scope=openid profile
- Client Credentials
grant_type=client_credentials
&client_id=client_id
&client_secret=secret_for_confidential_clients
&scope=api
- Refresh token
grant_type=refresh_token
&client_id=client_id
&refresh_token=token
Response:
{
"access_token": "eyJhbGci...",
"expires_in": 1800,
"refresh_expires_in": 86400,
"refresh_token": "eyJhbGci...",
"token_type": "Bearer",
"scope": "openid profile",
"id_token": "eyJhbGci...",
"session_state": "5adfe1762f184803a0b321c678c49b5b"
}
The ID token is present only when the requested scope contains openid. Client-credentials responses contain an access token but no refresh token or ID token.
The discovery response advertises client_secret_basic, but the current token endpoint reads client_secret from form data. Use client_secret_post in clients that require a secret.
User-info endpoint¶
Get authenticated user data.
GET /_bdk/api/identity/connect/userinfo
Authorization: Bearer token
Response:
{
"sub": "user_id",
"name": "User Name",
"given_name": "User",
"family_name": "Name",
"preferred_username": "user@example.com",
"email": "user@example.com",
"email_verified": true,
"roles": ["Admin", "User"]
}
Debug endpoint¶
Development information about configuration.
GET /_bdk/api/identity/connect/debuginfo
Response:
{
"tokenIssuer": "https://localhost:5001",
"tokenProvider": "Default",
"configuredClients": [
{
"clientId": "spa-client",
"name": "SPA App",
"redirectUris": ["http://localhost:4200/callback"],
"allowedScopes": ["openid", "profile", "email", "roles", "offline_access"]
}
],
"configuredUsers": [
{
"email": "user@example.com",
"name": "User Name",
"roles": ["Admin"]
}
]
}
Discovery endpoints¶
OpenID Connect discovery document.
GET /_bdk/api/identity/connect/.well-known/openid-configuration
GET /.well-known/openid-configuration
Response:
{
"issuer": "https://localhost:5001",
"authorization_endpoint": "https://localhost:5001/_bdk/api/identity/connect/authorize",
"token_endpoint": "https://localhost:5001/_bdk/api/identity/connect/token",
"userinfo_endpoint": "https://localhost:5001/_bdk/api/identity/connect/userinfo",
"end_session_endpoint": "https://localhost:5001/_bdk/api/identity/connect/logout",
"grant_types_supported": ["authorization_code", "password", "client_credentials", "refresh_token"],
"response_types_supported": ["code"],
"response_modes_supported": ["query", "form_post"],
"scopes_supported": ["openid", "profile", "email", "roles", "offline_access"],
"token_endpoint_auth_methods_supported": ["client_secret_post", "client_secret_basic", "none"]
}
Logout endpoint¶
Sign out of the provider cookie and optionally redirect back to a client:
GET /_bdk/api/identity/connect/logout?post_logout_redirect_uri=https%3A%2F%2Flocalhost%3A5001&state=abc123
The current implementation accepts the redirect URI directly; it does not validate it against the registered client redirect URIs. Use this endpoint only in the intended local development environment.
Error handling¶
OAuth 2.0 failures use responses such as:
{
"error": "invalid_request",
"error_description": "Error details"
}
Common error codes include invalid_request, invalid_client, invalid_grant, unsupported_response_type and unsupported_grant_type.
Security considerations¶
Development use only¶
- The provider is designed for development and testing.
- Users, passwords and client secrets are configured in application code.
- The provider does not implement the controls expected from a production identity service.
Default behaviors¶
- Access tokens expire after 24 hours and refresh tokens after 7 days unless configured otherwise.
- JWT signing is disabled when
SigningKeyis empty.WithSigningKey(...)enables HS256 signing. - Configured confidential-client secrets are validated for authorization-code and client-credentials grants.
- Client and redirect-URI validation applies when at least one client is configured.
- The named identity-provider CORS policy allows any origin, header and method.
- The debug endpoint exposes configured clients, users and endpoint URLs.
Cookie single sign-on¶
- 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)
Development tips¶
Custom client setup¶
// Multiple redirect URIs
.WithClient(
"Angular Frontend",
"angular-app",
["http://localhost:4200/callback",
"http://localhost:4200/silent-refresh"])
// API documentation tools
.WithClient(
"Swagger UI",
"swagger",
"https://localhost:5001/swagger/oauth2-redirect.html")
Testing scenarios¶
- Multiple users
- Role-based access
- Token validation
- Authentication flows
- Client registrations
Related resources¶
Additional request examples¶
This section retains expanded request and response examples for the same provider configuration described above.
Minimal-host characteristics¶
- OAuth 2.0 and OpenID Connect endpoints
- Authorization code, password, client credentials and refresh-token grants
- Configured users and clients without a database
- Local JWT generation
Minimal setup¶
Install the package¶
<PackageReference Include="BridgingIT.DevKit.Presentation.Web" Version="x.y.z" />
Configure services¶
In Program.cs:
builder.Services.AddFakeIdentityProvider(options => options
.Enabled(builder.Environment.IsDevelopment())
.WithIssuer("https://localhost:5001")
.WithUsers(Fakes.Users)
.WithGroupPath("/_bdk/api/identity/connect")
.WithClient("Angular application", "spa-client", "http://localhost:4200/callback"));
Define users¶
public static class Fakes
{
public static readonly FakeUser[] Users =
[
new("luke.skywalker@starwars.com", "Luke Skywalker",
[Role.Administrators, Role.Users],
password: "development-only",
isDefault: true),
new("yoda@starwars.com", "Yoda",
[Role.Administrators])
];
}
Client integration examples¶
Angular application (authorization-code flow)¶
Configure your Angular application with OIDC client library:
import { AuthConfig } from 'angular-oauth2-oidc';
export const authConfig: AuthConfig = {
issuer: 'https://localhost:5001',
redirectUri: window.location.origin + '/callback',
clientId: 'spa-client',
scope: 'openid profile email roles',
responseType: 'code'
};
Authorization-code flow¶
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
Direct API access (password grant)¶
POST /_bdk/api/identity/connect/token
Content-Type: application/x-www-form-urlencoded
grant_type=password
&client_id=spa-client
&username=luke.skywalker@starwars.com
&password=development-only
&scope=openid profile email roles
Service-to-service access (client credentials)¶
POST /_bdk/api/identity/connect/token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
&client_id=api-backend
&client_secret=api-secret
&scope=api
When clients are configured, the provider validates the client ID. It also validates the secret for configured confidential clients during authorization-code and client-credentials grants. If no clients are configured, the current implementation accepts an arbitrary non-empty client ID; configure clients when testing client validation.
Expanded API reference¶
Authorization endpoint¶
Starts the OAuth 2.0 authorization flow with user selection.
GET /_bdk/api/identity/connect/authorize?
response_type=code
&client_id=spa-client
&redirect_uri=http://localhost:4200/callback
&scope=openid profile email roles
&state=abc123
Token endpoint¶
Issues tokens using various grant types.
Authorization-code grant¶
POST /_bdk/api/identity/connect/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&client_id=spa-client
&code=xyz789
&redirect_uri=http://localhost:4200/callback
Sample 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"
}
User-info endpoint¶
Returns information about the authenticated user.
GET /_bdk/api/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"]
}
OpenID Connect configuration¶
Returns the OpenID Connect discovery document.
GET /_bdk/api/identity/connect/.well-known/openid-configuration
Response:
{
"issuer": "https://localhost:5001",
"authorization_endpoint": "https://localhost:5001/_bdk/api/identity/connect/authorize",
"token_endpoint": "https://localhost:5001/_bdk/api/identity/connect/token",
"userinfo_endpoint": "https://localhost:5001/_bdk/api/identity/connect/userinfo",
"end_session_endpoint": "https://localhost:5001/_bdk/api/identity/connect/logout",
"grant_types_supported": [
"authorization_code",
"password",
"client_credentials",
"refresh_token"
],
"response_types_supported": ["code"],
"response_modes_supported": ["query", "form_post"],
"scopes_supported": ["openid", "profile", "email", "roles", "offline_access"],
"claims_supported": [
"sub",
"name",
"family_name",
"given_name",
"preferred_username",
"email",
"email_verified",
"nonce"
],
"token_endpoint_auth_methods_supported": ["client_secret_post", "client_secret_basic", "none"]
}
Logout endpoint¶
Handles user logout with optional redirect.
GET /_bdk/api/identity/connect/logout?
post_logout_redirect_uri=http://localhost:4200
&state=abc123
Error response example¶
OAuth-related endpoint failures use an OAuth2Error response such as:
{
"error": "invalid_request",
"error_description": "The request is missing a required parameter"
}
The exact error codes depend on the endpoint and validation branch. See Error handling for the implemented common cases.
Development-only disclaimer¶
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 OAuth 2.0 and OpenID Connect flows without rigorous security validation. User credentials are stored in application configuration, authentication is simplified, and token validation is minimal. Security features such as rate limiting, audit logging and production session management are omitted.
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 provider is designed for development and testing.
WithSigningKey(...)enables symmetric HS256 signing; tokens are unsigned when the signing key is empty.- User authentication is intentionally simplified.
- Configured confidential-client secrets are validated only in the grant paths described above.