Cesivi Server - Authentication Guide¶
Version: 1.0 Last Updated: 2026-03-28 Related: PLAN-174 (Azure AD Mock & PnP 2.x Upgrade)
Table of Contents¶
- Overview
- OAuth 2.0 / Azure AD Mock
- Bearer Token Authentication
- Basic Authentication
- NTLM Authentication
- Forms Authentication
- PnP PowerShell 2.x Connection
- Troubleshooting
Overview¶
Cesivi Server supports multiple authentication methods to accommodate different client types and scenarios:
| Method | Use Case | Security Level | Client Support |
|---|---|---|---|
| OAuth 2.0 | Modern apps, PnP 2.x | High | PnP.PowerShell 2.x, custom apps |
| Bearer Token | API integrations | High | REST clients, CSOM |
| Basic Auth | Simple scenarios | Low (TLS required) | All clients |
| NTLM | Windows environments | Medium | Windows clients |
| Forms | Web applications | Medium | Browser-based apps |
Recommendation: Use OAuth 2.0 / Bearer tokens for production scenarios.
OAuth 2.0 / Azure AD Mock¶
Since PLAN-174, Cesivi includes a built-in Azure AD mock service that implements OAuth 2.0 / OpenID Connect (OIDC) endpoints. This enables modern SharePoint clients (like PnP.PowerShell 2.x) to authenticate without requiring a real Azure AD tenant.
Architecture¶
Client Application
↓
1. Discovery: GET /.well-known/openid-configuration
↓
2. Token Request: POST /oauth2/v2.0/token
↓
3. Receive JWT Access Token
↓
4. API Call: GET /_api/web (Authorization: Bearer {token})
↓
5. Server validates JWT signature and claims
↓
6. Response with requested data
Discovery Endpoint¶
The OpenID Connect discovery document describes the OAuth 2.0 capabilities:
Request:
GET /.well-known/openid-configuration HTTP/1.1
Host: localhost:5001
Response:
{
"issuer": "https://localhost:5001",
"authorization_endpoint": "https://localhost:5001/oauth2/v2.0/authorize",
"token_endpoint": "https://localhost:5001/oauth2/v2.0/token",
"jwks_uri": "https://localhost:5001/oauth2/v2.0/keys",
"response_types_supported": ["code", "token"],
"grant_types_supported": [
"authorization_code",
"client_credentials",
"refresh_token"
],
"token_endpoint_auth_methods_supported": [
"client_secret_post",
"client_secret_basic"
],
"scopes_supported": ["openid", "profile", "email"],
"claims_supported": ["sub", "name", "email", "roles"]
}
Token Endpoint¶
Client Credentials Flow (Service-to-Service)¶
Use this flow for non-interactive scenarios (background jobs, daemons, scripts):
Request:
POST /oauth2/v2.0/token HTTP/1.1
Host: localhost:5001
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&
client_id=12345678-1234-1234-1234-123456789012&
client_secret=mock-secret&
scope=https://localhost:5001/.default
Response:
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600
}
Authorization Code Flow (Interactive)¶
Use this flow for user-interactive scenarios:
Step 1: Authorization Request
GET /oauth2/v2.0/authorize?
client_id=12345678-1234-1234-1234-123456789012&
response_type=code&
redirect_uri=http://localhost:8080/callback&
scope=openid profile&
state=abc123 HTTP/1.1
Host: localhost:5001
Step 2: Authorization Response (redirect)
HTTP/1.1 302 Found
Location: http://localhost:8080/callback?code=AUTH_CODE_HERE&state=abc123
Step 3: Token Request
POST /oauth2/v2.0/token HTTP/1.1
Host: localhost:5001
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
code=AUTH_CODE_HERE&
client_id=12345678-1234-1234-1234-123456789012&
client_secret=mock-secret&
redirect_uri=http://localhost:8080/callback
Step 4: Token Response
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "REFRESH_TOKEN_HERE",
"token_type": "Bearer",
"expires_in": 3600
}
Refresh Token Flow¶
Use refresh tokens to obtain new access tokens without re-authentication:
Request:
POST /oauth2/v2.0/token HTTP/1.1
Host: localhost:5001
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&
refresh_token=REFRESH_TOKEN_HERE&
client_id=12345678-1234-1234-1234-123456789012&
client_secret=mock-secret
Response:
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "NEW_REFRESH_TOKEN",
"token_type": "Bearer",
"expires_in": 3600
}
JWT Token Structure¶
Access tokens are JSON Web Tokens (JWT) signed with RS256:
Header:
{
"alg": "RS256",
"typ": "JWT"
}
Payload:
{
"iss": "https://localhost:5001",
"aud": "https://localhost:5001/",
"sub": "user@cesivi.local",
"name": "Test User",
"email": "user@cesivi.local",
"roles": ["Site.FullControl.All"],
"scp": "AllSites.FullControl",
"exp": 1706875200,
"iat": 1706871600,
"nbf": 1706871600
}
Claims:
- iss - Issuer (Cesivi server URL)
- aud - Audience (target resource)
- sub - Subject (user identifier)
- name - User display name
- email - User email address
- roles - SharePoint permission roles
- scp - OAuth scopes
- exp - Expiration time (Unix timestamp)
- iat - Issued at time (Unix timestamp)
- nbf - Not before time (Unix timestamp)
Configuration¶
Configure Azure AD mock in appsettings.json:
{
"AzureAdMock": {
"Issuer": "https://localhost:5001",
"Audience": "https://localhost:5001/",
"TokenExpirationMinutes": 60,
"RefreshTokenExpirationHours": 168
},
"Authentication": {
"OAuth2": {
"Authority": "https://localhost:5001",
"Audience": "https://localhost:5001/",
"ValidateIssuer": true,
"ValidateAudience": true,
"ValidateLifetime": true,
"ValidateIssuerSigningKey": true
}
}
}
Bearer Token Authentication¶
Bearer tokens are the recommended authentication method for API clients.
Request:
GET /_api/web HTTP/1.1
Host: localhost:5001
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
Accept: application/json
Response:
{
"d": {
"Title": "Team Site",
"Url": "http://localhost:5001/Default/RootSite"
}
}
Basic Authentication¶
Simple username/password authentication. Requires HTTPS in production.
Request:
GET /_api/web HTTP/1.1
Host: localhost:5001
Authorization: Basic dXNlcjpwYXNzd29yZA==
Accept: application/json
The Authorization header contains Basic followed by Base64-encoded username:password.
Example (PowerShell):
$username = "user@cesivi.local"
$password = "password"
$pair = "$($username):$($password)"
$bytes = [System.Text.Encoding]::ASCII.GetBytes($pair)
$base64 = [System.Convert]::ToBase64String($bytes)
$headers = @{
"Authorization" = "Basic $base64"
"Accept" = "application/json"
}
Invoke-RestMethod -Uri "http://localhost:5000/_api/web" -Headers $headers
Testing tip: for named, predictable test users (not just whatever AcceptAll accepts), enable the ConfigUsers provider — a static, config-defined Basic/Forms user list with optional nested groups. See IDENTITY_PROVIDERS.md § ConfigUsers Provider. Never enable it in production — passwords are stored in plaintext.
NTLM Authentication¶
Windows integrated authentication using NTLM.
PowerShell Example:
$cred = Get-Credential
Invoke-RestMethod -Uri "http://localhost:5000/_api/web" `
-Credential $cred `
-Authentication Negotiate
CSOM Example (C#):
using (var ctx = new ClientContext("http://localhost:5000"))
{
ctx.Credentials = CredentialCache.DefaultNetworkCredentials;
var web = ctx.Web;
ctx.Load(web);
ctx.ExecuteQuery();
Console.WriteLine(web.Title);
}
Forms Authentication¶
Cookie-based authentication for web applications.
Login:
POST /_forms/default.aspx HTTP/1.1
Host: localhost:5001
Content-Type: application/x-www-form-urlencoded
username=user@cesivi.local&password=password
Response:
HTTP/1.1 302 Found
Set-Cookie: FedAuth=TOKEN_HERE; Path=/; HttpOnly
Location: /
Subsequent Requests:
GET /_api/web HTTP/1.1
Host: localhost:5001
Cookie: FedAuth=TOKEN_HERE
Accept: application/json
Forms login validates against whichever enabled provider supports the
Formsscheme — including ConfigUsers for named test users. See IDENTITY_PROVIDERS.md § ConfigUsers Provider.
PnP PowerShell 2.x Connection¶
Using Access Token (Recommended)¶
Step 1: Obtain Access Token
# Using Cesivi Azure AD mock
$tokenResponse = Invoke-RestMethod -Method Post `
-Uri "https://localhost:5001/oauth2/v2.0/token" `
-Body @{
grant_type = "client_credentials"
client_id = "12345678-1234-1234-1234-123456789012"
client_secret = "mock-secret"
scope = "https://localhost:5001/.default"
}
$accessToken = ConvertTo-SecureString $tokenResponse.access_token -AsPlainText -Force
Step 2: Connect to SharePoint
Connect-PnPOnline -Url "https://localhost:5001" -AccessToken $accessToken
Step 3: Use PnP Cmdlets
# Now you can use any PnP cmdlet
Get-PnPWeb
Get-PnPList
Get-PnPListItem -List "Documents"
Complete Example¶
# Full PnP 2.x connection workflow
function Connect-CesiviPnP {
param(
[string]$Url = "https://localhost:5001",
[string]$ClientId = "12345678-1234-1234-1234-123456789012",
[string]$ClientSecret = "mock-secret"
)
# Get access token from Azure AD mock
$tokenUrl = "$Url/oauth2/v2.0/token"
$tokenResponse = Invoke-RestMethod -Method Post -Uri $tokenUrl -Body @{
grant_type = "client_credentials"
client_id = $ClientId
client_secret = $ClientSecret
scope = "$Url/.default"
}
# Convert to secure string
$accessToken = ConvertTo-SecureString $tokenResponse.access_token -AsPlainText -Force
# Connect
Connect-PnPOnline -Url $Url -AccessToken $accessToken
Write-Host "✅ Connected to $Url" -ForegroundColor Green
}
# Usage
Connect-CesiviPnP
$web = Get-PnPWeb
Write-Host "Connected to: $($web.Title)" -ForegroundColor Cyan
Credential-Based Connection (Legacy)¶
For testing only. PnP 2.x prefers token-based authentication.
$cred = Get-Credential
Connect-PnPOnline -Url "http://localhost:5000" -Credentials $cred
Troubleshooting¶
Token Expired¶
Symptom: 401 Unauthorized responses
Solution: Refresh your access token using the refresh token flow, or obtain a new token.
# Check token expiration
$tokenParts = $tokenResponse.access_token.Split('.')
$payload = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($tokenParts[1]))
$claims = $payload | ConvertFrom-Json
$expiration = [DateTimeOffset]::FromUnixTimeSeconds($claims.exp).LocalDateTime
Write-Host "Token expires at: $expiration"
Invalid Audience¶
Symptom: 401 Unauthorized with "Invalid audience" error
Solution: Ensure the scope parameter matches the server's configured audience:
# Correct: Use server URL with /.default
scope = "https://localhost:5001/.default"
# Incorrect:
scope = "https://cesivi.local/.default" # Wrong audience
Certificate Trust Issues¶
Symptom: SSL/TLS certificate validation errors
Solution (Development Only):
# Disable certificate validation for localhost testing (DEV ONLY!)
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}
# Better: Install self-signed certificate in Trusted Root store
PnP Connection Fails¶
Symptom: Connect-PnPOnline fails with authentication error
Solution: Verify token is valid and not expired:
# Test token with direct REST API call first
$headers = @{
"Authorization" = "Bearer $($tokenResponse.access_token)"
"Accept" = "application/json"
}
Invoke-RestMethod -Uri "https://localhost:5001/_api/web" -Headers $headers
# If this works but Connect-PnPOnline fails, check PnP version
Get-Module PnP.PowerShell -ListAvailable
CORS Issues¶
Symptom: Browser-based apps fail with CORS errors
Solution: Cesivi automatically configures CORS for localhost. For other origins, update appsettings.json:
{
"Cors": {
"AllowedOrigins": ["http://localhost:3000", "https://myapp.example.com"],
"AllowCredentials": true
}
}
Security Best Practices¶
- Always use HTTPS in production - Never send credentials over unencrypted HTTP
- Rotate secrets regularly - Change client secrets periodically
- Limit token lifetime - Use short-lived access tokens (1 hour or less)
- Use refresh tokens - Avoid storing long-lived credentials
- Validate tokens - Always validate issuer, audience, and expiration
- Secure storage - Never store tokens or secrets in source code
- TLS 1.2+ - Disable older SSL/TLS versions
Related Documentation¶
- API_REFERENCE.md - REST API endpoints
- IDENTITY_PROVIDERS.md - Identity provider configuration
- PNP_COMPATIBILITY.md - PnP PowerShell compatibility
- PLAN-174 - Implementation details
Last Updated: 2026-03-28 PLAN-174 Phase 6: Testing & Documentation