Masar Eagle implements a standards-based authentication system using OpenID Connect (via OpenIddict) with support for multiple authentication flows: phone-based OTP for drivers and passengers, password-based login for admins and companies, and refresh token rotation for long-lived sessions.
Authentication Architecture
OpenID Connect with OpenIddict
The Identity service uses OpenIddict to implement OpenID Connect:
src/services/Identity/src/Identity.Web/OpenIddictServerConfiguration.cs:16
services . AddOpenIddict ()
. AddServer ( options =>
{
options . SetTokenEndpointUris ( "connect/token" )
. SetRevocationEndpointUris ( "connect/revocation" )
. SetIntrospectionEndpointUris ( "connect/introspect" );
options . AllowPasswordFlow ()
. AllowRefreshTokenFlow ()
. AllowCustomFlow ( "urn:masareagle:otp" ); // Custom OTP grant
options . SetIssuer ( new Uri ( $"https:// { jwt . Issuer } " ));
// Persistent RSA keys for token signing/encryption
AddPersistentSigningKey ( options );
AddPersistentEncryptionKey ( options );
options . DisableAccessTokenEncryption ();
options . SetAccessTokenLifetime ( TimeSpan . FromMinutes ( jwt . AccessTokenExpiryMinutes ));
options . SetRefreshTokenLifetime ( TimeSpan . FromDays ( 30 ));
options . RegisterScopes (
OpenIddictConstants . Scopes . OpenId ,
OpenIddictConstants . Scopes . OfflineAccess ,
OpenIddictConstants . Scopes . Profile ,
OpenIddictConstants . Scopes . Roles ,
"api" );
})
. AddValidation ( options =>
{
options . UseLocalServer (); // Validate tokens locally
options . UseAspNetCore ();
});
Why OpenIddict? It’s a lightweight, standards-compliant OIDC implementation that supports custom grant types—perfect for mobile apps requiring OTP authentication.
Persistent Signing Keys
Tokens are signed with persistent RSA keys that survive restarts:
src/services/Identity/src/Identity.Web/OpenIddictServerConfiguration.cs:117
private static void AddPersistentSigningKey ( OpenIddictServerBuilder options )
{
Directory . CreateDirectory ( KeysDirectory );
var signingKeyPath = Path . Combine ( KeysDirectory , "signing-key.pem" );
RSA rsa ;
if ( File . Exists ( signingKeyPath ))
{
rsa = RSA . Create ();
rsa . ImportRSAPrivateKey (
Convert . FromBase64String ( File . ReadAllText ( signingKeyPath )), out _ );
}
else
{
rsa = RSA . Create ( 2048 );
var pem = Convert . ToBase64String ( rsa . ExportRSAPrivateKey ());
File . WriteAllText ( signingKeyPath , pem );
}
options . AddSigningKey ( new RsaSecurityKey ( rsa ));
}
Production Deployment : Mount a persistent volume to /keys (configured via IDENTITY_KEYS_PATH environment variable) to ensure keys survive container restarts.
Authentication Flows
1. OTP Flow (Driver/Passenger)
Phone-based authentication using one-time passwords:
Request OTP
Client sends phone number to request an OTP: POST /api/auth/send-otp
Content-Type: application/json
{
"phoneNumber" : "+966501234567",
"userType" : "driver"
}
The Identity service generates a 6-digit code and sends it via SMS.
Submit OTP
Client submits the OTP code to the token endpoint: POST /connect/token
Content-Type: application/x-www-form-urlencoded
grant_type = urn:masareagle:otp &
phone_number = +966501234567 &
otp_code = 123456 &
user_type = driver
Verify OTP
The Identity service verifies the OTP code: src/services/Identity/src/Identity.Web/TokenEndpoint.cs:87
private static async Task < IResult > VerifyOtpCode (
PhoneNumber phone ,
string code ,
string userType ,
IOtpService otpService ,
IUserPhoneResolver phoneResolver ,
IMessageBus messageBus )
{
var otpResult = await otpService . VerifyOtpAsync ( phone . Value , code );
if ( otpResult . Status != ResultStatus . Ok )
{
return ForbidWithError ( Errors . InvalidGrant ,
string . Join ( "; " , otpResult . Errors ));
}
// Publish event to ensure user is provisioned
await messageBus . PublishAsync (
new UserAuthenticatedEvent ( phone . Value , userType , phone . Value ));
var userIdResult = await phoneResolver . ResolveUserIdAsync ( phone . Value , userType );
return await SignInAndPublish ( userIdResult . Value , userType , phone . Value , messageBus );
}
Issue Tokens
Upon successful verification, the service issues tokens: {
"access_token" : "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." ,
"token_type" : "Bearer" ,
"expires_in" : 3600 ,
"refresh_token" : "CfDJ8..." ,
"scope" : "openid offline_access profile roles api"
}
User Provisioning
The Identity service publishes a UserAuthenticatedEvent to RabbitMQ: await messageBus . PublishAsync (
new UserAuthenticatedEvent ( userId , userType , phoneNumber ));
The Users service handles this event to create or update the user record.
Race Condition Handling : If the user isn’t provisioned yet, the Identity service waits 1 second and retries:src/services/Identity/src/Identity.Web/TokenEndpoint.cs:106
if ( userIdResult . Status != ResultStatus . Ok )
{
// Race condition: user not provisioned yet—wait and retry
await Task . Delay ( 1000 );
userIdResult = await phoneResolver . ResolveUserIdAsync ( phone . Value , userType );
}
2. Password Flow (Admin/Company)
Traditional email/password authentication for administrative accounts:
Submit Credentials
Client sends email and password: POST /connect/token
Content-Type: application/x-www-form-urlencoded
grant_type = password &
username = [email protected] &
password = SecurePassword123 &
user_type = admin
Verify Credentials
The Identity service validates credentials against the Users database: src/services/Identity/src/Identity.Web/TokenEndpoint.cs:122
private static async Task < IResult > HandlePasswordGrant (
OpenIddictRequest request ,
IUserCredentialVerifier credentialVerifier )
{
var userType = request . GetParameter ( "user_type" ) ? . ToString ();
return userType switch
{
null or "" => ForbidWithError ( Errors . InvalidRequest , "نوع المستخدم مطلوب" ),
not ( UserTypes . Admin or UserTypes . Company ) =>
ForbidWithError ( Errors . InvalidGrant ,
"نوع المستخدم غير مدعوم لتسجيل الدخول بكلمة مرور" ),
_ => await VerifyCredentials ( request . Username ! , request . Password ! ,
userType , credentialVerifier )
};
}
Restricted Grant Type : Password flow is only allowed for admin and company user types. Drivers and passengers must use OTP.
Issue Tokens
Tokens are issued with the same structure as OTP flow.
3. Refresh Token Flow
All user types can refresh their access tokens without re-authenticating:
Submit Refresh Token
POST /connect/token
Content-Type: application/x-www-form-urlencoded
grant_type = refresh_token &
refresh_token = CfDJ8...
Validate Refresh Token
src/services/Identity/src/Identity.Web/TokenEndpoint.cs:152
private static async Task < IResult > HandleRefreshGrant ( HttpContext context )
{
var result = await context . AuthenticateAsync (
OpenIddictServerAspNetCoreDefaults . AuthenticationScheme );
return result switch
{
{ Succeeded : false } or { Principal : null } =>
ForbidWithError ( Errors . InvalidGrant ,
"رمز التحديث غير صالح أو منتهي الصلاحية" ),
_ => RefreshIdentity ( result . Principal ! )
};
}
Issue New Tokens
A new access token is issued (refresh token may also be rotated).
JWT Token Structure
Access tokens are JSON Web Tokens (JWT) containing claims about the authenticated user:
src/services/Identity/src/Identity.Web/TokenEndpoint.cs:187
private static IResult SignInWithIdentity (
string userId ,
string userType )
{
var identity = new ClaimsIdentity (
authenticationType : TokenValidationParameters . DefaultAuthenticationType ,
nameType : Claims . Name ,
roleType : Claims . Role );
identity . SetClaim ( Claims . Subject , userId ) // User ID
. SetClaim ( Claims . Role , userType ); // "driver", "passenger", "admin", "company"
identity . SetScopes (
Scopes . OpenId ,
Scopes . OfflineAccess , // Required for refresh tokens
Scopes . Roles ,
"api" );
identity . SetAudiences ( "masar-eagle-api" );
// Control which claims go into which token
identity . SetDestinations ( static claim => claim . Type switch
{
Claims . Subject => [ Destinations . AccessToken , Destinations . IdentityToken ],
Claims . Role => [ Destinations . AccessToken , Destinations . IdentityToken ],
_ => [ Destinations . AccessToken ]
});
return Results . SignIn (
new ClaimsPrincipal ( identity ),
authenticationScheme : OpenIddictServerAspNetCoreDefaults . AuthenticationScheme );
}
Decoded JWT Example
{
"sub" : "d_0191234567890abcd" ,
"role" : "driver" ,
"aud" : "masar-eagle-api" ,
"iss" : "https://identity.masareagle.com" ,
"scope" : "openid offline_access profile roles api" ,
"exp" : 1735689600 ,
"iat" : 1735686000
}
Claim Mapping Disabled : The Identity service sets MapInboundClaims = false to preserve standard claim names like sub and role instead of .NET’s default mapping.
Token Validation in Backend Services
All backend services (Users, Trips, Notifications) validate JWT tokens using JWKS discovery:
src/BuildingBlocks/Common/Authentication/AuthenticationExtensions.cs:11
public static IServiceCollection AddAppAuthentication (
this IServiceCollection services ,
IConfiguration configuration )
{
var identityUrl = configuration [ "services:identity:http:0" ]
?? configuration [ "services:identity:https:0" ]
?? throw new InvalidOperationException (
"Identity URL not configured" );
services . AddAuthentication ( JwtBearerDefaults . AuthenticationScheme )
. AddJwtBearer ( options =>
{
options . RequireHttpsMetadata = false ;
options . MapInboundClaims = false ;
options . MetadataAddress =
$" { identityUrl . TrimEnd ( '/' )} /.well-known/openid-configuration" ;
options . TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false , // Same audience for all services
RoleClaimType = Roles . ClaimType , // "role"
NameClaimType = "sub" // Use subject as name
};
});
return services . AddPolicies ();
}
JWKS Discovery
Services discover the Identity service’s public keys via OpenID Connect metadata: GET https://identity/.well-known/openid-configuration
GET https://identity/.well-known/jwks
Token Validation
Each service validates incoming JWTs:
Signature verification using JWKS public key
Expiration time check
Issuer validation
Claims Extraction
Validated claims are available in ClaimsPrincipal: var userId = httpContext . User . FindFirst ( "sub" ) ? . Value ;
var userRole = httpContext . User . FindFirst ( "role" ) ? . Value ;
Authorization Policies
The system defines role-based authorization policies:
src/BuildingBlocks/Common/Authentication/AuthenticationExtensions.cs:46
private static IServiceCollection AddPolicies ( this IServiceCollection services )
{
services . AddAuthorization ( options =>
{
options . AddPolicy ( Policies . Admin ,
p => p . RequireRole ( Roles . Admin ));
options . AddPolicy ( Policies . Company ,
p => p . RequireRole ( Roles . Company ));
options . AddPolicy ( Policies . Driver ,
p => p . RequireRole ( Roles . Driver ));
options . AddPolicy ( Policies . Passenger ,
p => p . RequireRole ( Roles . Passenger ));
options . AddPolicy ( Policies . AdminOrDriver ,
p => p . RequireRole ( Roles . Admin , Roles . Driver ));
options . AddPolicy ( Policies . DriverOrPassenger ,
p => p . RequireRole ( Roles . Driver , Roles . Passenger ));
options . AddPolicy ( Policies . AdminOrDriverOrPassenger ,
p => p . RequireRole ( Roles . Admin , Roles . Driver , Roles . Passenger ));
});
return services ;
}
Using Policies in Endpoints
app . MapGet ( "/api/drivers/me" ,
[ Authorize ( Policy = Policies . Driver )] async ( ClaimsPrincipal user ) =>
{
var driverId = user . FindFirst ( "sub" ) ? . Value ;
// ...
})
. RequireAuthorization ( Policies . Driver );
Token Revocation
The Identity service supports token revocation:
POST /connect/revocation
Content-Type: application/x-www-form-urlencoded
token = CfDJ8... &
token_type_hint = refresh_token
Access tokens cannot be revoked before expiration (stateless JWT design). Only refresh tokens can be revoked.
Security Best Practices
Short-Lived Access Tokens Access tokens expire quickly (default: minutes) to limit exposure if compromised.
Persistent Signing Keys RSA keys are stored in persistent volumes to prevent token invalidation on restart.
HTTPS Enforcement Production deployments must use HTTPS for all authentication endpoints.
Refresh Token Rotation Refresh tokens are rotated on use (new refresh token issued each time).
OTP Rate Limiting Implement rate limiting on /api/auth/send-otp to prevent SMS abuse.
Token Blacklisting Users service maintains a blacklist table for revoked tokens.
Configuration
JWT Settings
{
"Jwt" : {
"Issuer" : "identity.masareagle.com" ,
"AccessTokenExpiryMinutes" : 60
}
}
Environment Variables
IDENTITY_KEYS_PATH = /keys # Persistent key storage
ASPNETCORE_ENVIRONMENT = Production
OTEL_EXPORTER_OTLP_ENDPOINT = http://otelcollector:4317
Troubleshooting
Symptom : Services return 401 UnauthorizedSolutions :
Verify Identity service is running and accessible
Check JWKS endpoint: curl https://identity/.well-known/jwks
Ensure services have correct Identity URL in configuration
Verify token hasn’t expired: decode JWT at jwt.io
Symptom : Users don’t receive SMS OTP codesSolutions :
Check SMS provider configuration
Verify phone number format (+966501234567)
Check Identity service logs for SMS sending errors
Implement retry mechanism in client
Tokens Invalid After Restart
Symptom : All tokens become invalid when Identity service restartsSolutions :
Mount persistent volume to /keys directory
Set IDENTITY_KEYS_PATH environment variable
Verify keys directory is writable
Check that RSA keys are loaded from disk, not regenerated
Services Overview Identity service responsibilities and architecture
Microservices Architecture How services communicate securely
API Reference Complete authentication API documentation