Skip to content

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

  1. Overview
  2. OAuth 2.0 / Azure AD Mock
  3. Bearer Token Authentication
  4. Basic Authentication
  5. NTLM Authentication
  6. Forms Authentication
  7. PnP PowerShell 2.x Connection
  8. 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 Forms scheme — including ConfigUsers for named test users. See IDENTITY_PROVIDERS.md § ConfigUsers Provider.


PnP PowerShell 2.x Connection

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

  1. Always use HTTPS in production - Never send credentials over unencrypted HTTP
  2. Rotate secrets regularly - Change client secrets periodically
  3. Limit token lifetime - Use short-lived access tokens (1 hour or less)
  4. Use refresh tokens - Avoid storing long-lived credentials
  5. Validate tokens - Always validate issuer, audience, and expiration
  6. Secure storage - Never store tokens or secrets in source code
  7. TLS 1.2+ - Disable older SSL/TLS versions


Last Updated: 2026-03-28 PLAN-174 Phase 6: Testing & Documentation