Skip to main content

Authentication Architecture

OpenPrime uses Keycloak for authentication, implementing OpenID Connect (OIDC) with PKCE flow.

Authentication Flow​

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Browserβ”‚ β”‚ Frontend β”‚ β”‚ Keycloak β”‚ β”‚ Backend β”‚
β””β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜
β”‚ β”‚ β”‚ β”‚
β”‚ 1. Visit App β”‚ β”‚ β”‚
│─────────────────▢│ β”‚ β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ 2. Check Auth β”‚ β”‚
β”‚ │──────────────────▢│ β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ 3. Redirect to Login β”‚ β”‚
│◀─────────────────────────────────────│ β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ 4. User Login β”‚ β”‚ β”‚
│─────────────────────────────────────▢│ β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ 5. Auth Code + Redirect β”‚ β”‚
│◀─────────────────────────────────────│ β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ 6. Exchange Code β”‚ β”‚
β”‚ │──────────────────▢│ β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ 7. JWT Tokens β”‚ β”‚
β”‚ │◀──────────────────│ β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ 8. API Request (Bearer Token) β”‚
β”‚ │─────────────────────────────────────▢│
β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ 9. Validate JWT β”‚
β”‚ β”‚ │◀─────────────────│
β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ 10. Response β”‚ β”‚
β”‚ │◀─────────────────────────────────────│
β”‚ β”‚ β”‚ β”‚

Components​

Frontend (keycloak-js)​

// Keycloak initialization
import Keycloak from 'keycloak-js';

const keycloak = new Keycloak({
url: 'http://localhost:8080',
realm: 'openprime',
clientId: 'openprime-app'
});

// Initialize with PKCE
await keycloak.init({
onLoad: 'check-sso',
pkceMethod: 'S256',
checkLoginIframe: false
});

Backend (keycloak-connect)​

// JWT validation middleware
const Keycloak = require('keycloak-connect');

const keycloak = new Keycloak({}, {
realm: 'openprime',
'auth-server-url': 'http://localhost:8080',
'ssl-required': 'external',
resource: 'openprime-backend',
'bearer-only': true
});

app.use(keycloak.middleware());
app.use('/api', keycloak.protect());

Keycloak Configuration​

Realm: openprime​

{
"realm": "openprime",
"enabled": true,
"sslRequired": "external",
"registrationAllowed": true,
"loginWithEmailAllowed": true,
"duplicateEmailsAllowed": false,
"resetPasswordAllowed": true,
"editUsernameAllowed": false,
"bruteForceProtected": true
}

Client: openprime-app (Frontend)​

{
"clientId": "openprime-app",
"enabled": true,
"publicClient": true,
"standardFlowEnabled": true,
"directAccessGrantsEnabled": false,
"rootUrl": "http://localhost:3000",
"redirectUris": ["http://localhost:3000/*"],
"webOrigins": ["http://localhost:3000"],
"protocol": "openid-connect"
}

Client: openprime-backend (API)​

{
"clientId": "openprime-backend",
"enabled": true,
"bearerOnly": true,
"publicClient": false
}

Token Structure​

Access Token Claims​

{
"exp": 1699999999,
"iat": 1699996399,
"jti": "unique-token-id",
"iss": "http://localhost:8080/realms/openprime",
"sub": "user-uuid",
"typ": "Bearer",
"azp": "openprime-app",
"preferred_username": "john.doe",
"email": "john.doe@example.com",
"email_verified": true,
"realm_access": {
"roles": ["user"]
}
}

User Provisioning​

Users are automatically created in OpenPrime database on first authenticated request:

// Backend middleware
async function ensureUserExists(req, res, next) {
const keycloakId = req.kauth.grant.access_token.content.sub;
const username = req.kauth.grant.access_token.content.preferred_username;
const email = req.kauth.grant.access_token.content.email;

let user = await User.findOne({ where: { keycloakId } });

if (!user) {
user = await User.create({
keycloakId,
username,
email
});
}

req.user = user;
next();
}

Token Refresh​

The frontend automatically refreshes tokens before expiry:

// Auto-refresh configuration
keycloak.onTokenExpired = () => {
keycloak.updateToken(30)
.then(refreshed => {
if (refreshed) {
console.log('Token refreshed');
}
})
.catch(() => {
console.error('Failed to refresh token');
keycloak.login();
});
};

Session Management​

Logout​

// Frontend logout
keycloak.logout({
redirectUri: window.location.origin
});

Silent Check SSO​

// Check if user is authenticated without prompting
keycloak.init({
onLoad: 'check-sso',
silentCheckSsoRedirectUri:
window.location.origin + '/silent-check-sso.html'
});

Security Considerations​

PKCE (Proof Key for Code Exchange)​

  • Prevents authorization code interception attacks
  • Required for public clients (SPAs)
  • Uses SHA-256 code challenge method

Token Storage​

  • Access tokens stored in memory (not localStorage)
  • Refresh tokens handled by Keycloak JS
  • No sensitive data in browser storage

CORS Configuration​

// Backend CORS settings
app.use(cors({
origin: ['http://localhost:3000'],
credentials: true
}));

Rate Limiting​

// Limit authentication attempts
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // limit each IP to 100 requests per windowMs
});

Identity Federation​

Keycloak supports external identity providers:

  • Social: Google, GitHub, Microsoft
  • Enterprise: SAML 2.0, LDAP, Active Directory
  • Custom: OpenID Connect providers

Configure in Keycloak Admin β†’ Identity Providers.