Okta SSO Integration¶
Version: 1.0
Updated: 2026-04-09
Classification: CONFIDENTIAL — Internal Use Only
Overview¶
All GPUS IT portals are protected by Okta Single Sign-On using OIDC Authorization Code + PKCE flow. A single Okta application ("Greenpeace.US") provides authentication for all portals through a shared JavaScript auth module (gpus-okta-auth.js).
Protocol: OpenID Connect (OIDC)
Grant Type: Authorization Code with PKCE
Client Type: Single-Page Application (public client, no client secret)
Token Storage: sessionStorage (cleared on tab close)
Architecture¶
User → Browser → status.greenpeace.us (nginx/Cloud Run)
│
├── gpus-okta-auth.js checks session
│
├── No session → Okta login page
│ │
│ └── User authenticates → Okta redirects back
│ with authorization code
│
├── PKCE exchange → access_token + id_token
│
└── Authenticated → render dashboard + user bar
All portals use the same flow. The auth module detects the current origin and uses it as the redirect URI.
Okta Application Configuration¶
| Setting | Value |
|---|---|
| App Name | Greenpeace.US |
| App Type | Single-Page Application (SPA) |
| Grant Type | Authorization Code |
| PKCE | Required (enforced) |
| Client Authentication | None (public client) |
| Client ID | 0oadhpjktd5UfCMDm0x7 |
| Okta Domain (Preview) | greenpeaceeu.oktapreview.com |
| Authorization Server | Org Authorization Server (not a Custom Authorization Server) |
| Issuer | https://greenpeaceeu.oktapreview.com |
Redirect URIs¶
Sign-in redirect URIs:
https://status.greenpeace.ushttps://soc.greenpeace.ushttps://infra.greenpeace.us
Sign-out redirect URIs:
https://status.greenpeace.ushttps://soc.greenpeace.ushttps://infra.greenpeace.us
Adding new portals
When a new portal goes live (e.g., forms.greenpeace.us), add its URL to both sign-in and sign-out redirect URI lists in the Okta admin console under Applications → Greenpeace.US → General Settings → Edit.
Shared Auth Module — gpus-okta-auth.js¶
The gpus-okta-auth.js module handles the full OIDC PKCE lifecycle for all portals:
- Checks for existing authenticated session
- If no session → shows branded login overlay with "Sign in with Okta" button
- Redirects to Okta for authentication
- Handles the callback (exchanges authorization code for tokens via PKCE)
- Extracts user profile (name, email, groups)
- Injects user info bar into page header with sign-out button
Integration (add to any portal HTML)¶
Add these two script tags in the <head>, before any other scripts:
<!-- Okta Auth JS SDK (CDN) -->
<script src="https://global.oktacdn.com/okta-auth-js/7.5.1/okta-auth-js.min.js"
type="text/javascript"></script>
<!-- GPUS shared auth wrapper -->
<script src="gpus-okta-auth.js"></script>
Then initialize at the bottom of <body>, before closing </body>:
<script>
gpusAuth.init({
onReady: function(user) {
console.log('Authenticated:', user.name, user.email);
// Start your app logic here
},
onError: function(err) {
console.error('Auth error:', err);
}
});
</script>
Configuration Override (optional)¶
To override defaults (e.g., custom redirect URI), set GPUS_OKTA_CONFIG before loading the auth script:
<script>
window.GPUS_OKTA_CONFIG = {
domain: 'greenpeaceeu.oktapreview.com',
clientId: '0oadhpjktd5UfCMDm0x7',
redirectUri: 'https://custom-portal.greenpeace.us/',
scopes: ['openid', 'profile', 'email', 'groups']
};
</script>
Public API¶
| Method | Description |
|---|---|
gpusAuth.init(options) |
Initialize auth flow. Options: onReady(user), onError(err) |
gpusAuth.login() |
Trigger Okta login redirect |
gpusAuth.logout() |
Sign out from Okta and redirect to portal |
gpusAuth.getUser() |
Returns {name, email, sub, groups} or null |
gpusAuth.getAccessToken() |
Returns current access token string or null |
gpusAuth.config |
Read-only config object for debugging |
Per-Site Integration Status¶
| Portal | URL | Auth Integrated | Notes |
|---|---|---|---|
| Status Dashboard | status.greenpeace.us | ✅ | First implementation |
| SOC Dashboard | soc.greenpeace.us | ✅ | Same pattern |
| Infra Docs (MkDocs) | infra.greenpeace.us | ✅ | Custom theme override |
| Forms Portal | forms.greenpeace.us | ⬜ Pending | Add redirect URI when ready |
Security Details¶
Why the Org Authorization Server?¶
This integration uses Okta's Org Authorization Server (issuer: https://greenpeaceeu.oktapreview.com) rather than a Custom Authorization Server (which would have issuer path /oauth2/<name>). Key implications:
- No custom scopes or claims — only standard OIDC scopes (
openid,profile,email,groups) are available - Access tokens are opaque — not self-contained JWTs. They can only be validated by calling Okta's
/userinfoendpoint - ID tokens are JWTs — signed by Okta and verifiable via the JWKS endpoint at
https://greenpeaceeu.oktapreview.com/oauth2/v1/keys - Sufficient for current needs — all portals only need user identity (name, email, groups) which the ID token provides
- Phase 2 consideration — backend JWT validation will use the ID token (JWT) rather than the access token (opaque), or migrate to a Custom Authorization Server if API-specific scopes are needed
Why OIDC + PKCE over SAML?¶
SAML requires a server-side backend to receive and validate XML assertions. All GPUS portals are static SPAs served by nginx on Cloud Run — there is no server-side session layer. OIDC with PKCE was designed specifically for this architecture:
- No client secret — safe for browser-based apps (public clients)
- PKCE — prevents authorization code interception (code_verifier/code_challenge)
- JWT tokens — signed by Okta, verifiable client-side without server roundtrip
- Session storage — tokens cleared on tab close, no persistent local storage
Token Lifecycle¶
| Token | Lifetime | Storage | Purpose |
|---|---|---|---|
| ID Token | 1 hour | sessionStorage | User identity (name, email, groups) |
| Access Token | 1 hour | sessionStorage | API authorization (Phase 2) |
| Refresh Token | 24 hours | sessionStorage | Silent session renewal |
What PKCE Protects Against¶
- Authorization code interception — the code alone is useless without the code_verifier
- Cross-site request forgery — state parameter validated on callback
- Token leakage — sessionStorage is origin-bound and not sent in HTTP requests
Production Cutover Checklist¶
When production Okta tenant is provisioned:
- [ ] Create new SPA app integration in production Okta (same settings)
- [ ] Copy new Client ID
- [ ] Note new Okta domain (e.g.,
greenpeace.okta.com) - [ ] Update
gpus-okta-auth.js— changeOKTA_DOMAINandCLIENT_ID - [ ] Add all portal redirect URIs in production Okta app
- [ ] Assign users/groups in production Okta
- [ ] Deploy updated
gpus-okta-auth.jsto all portals - [ ] Test login flow on each portal
- [ ] Verify sign-out redirects correctly
- [ ] Update this document with production values
- [ ] Remove Okta Preview app integration (or keep for dev/test)
Phase 2 — Backend Token Validation (Future)¶
When ready to secure the API backends (status-backend, soc-backend):
- Frontend sends the ID token (JWT) in
Authorization: Bearer <token>header - Backend validates JWT signature against Okta JWKS endpoint (
/oauth2/v1/keys) - Backend checks issuer, audience (client_id), and expiry claims
- Reject unauthenticated requests with 401
Org Auth Server — ID Token, Not Access Token
Because we use the Org Authorization Server, access tokens are opaque (not JWTs). Backend validation must use the ID token instead, or call Okta's /userinfo endpoint with the access token. If API-specific scopes or custom claims become necessary, migrate to a Custom Authorization Server.
This adds defense-in-depth — even if someone discovers the backend URL, they cannot access data without a valid Okta token.
# Flask middleware example (Phase 2)
# Validates the Okta ID token (JWT) from the Org Authorization Server
import requests
from jose import jwt, JWTError
OKTA_ISSUER = 'https://greenpeaceeu.oktapreview.com'
OKTA_CLIENT_ID = '0oadhpjktd5UfCMDm0x7'
JWKS_URL = f'{OKTA_ISSUER}/oauth2/v1/keys'
# Cache JWKS keys
_jwks_cache = None
def get_jwks():
global _jwks_cache
if not _jwks_cache:
_jwks_cache = requests.get(JWKS_URL).json()
return _jwks_cache
@app.before_request
def verify_token():
if request.endpoint == 'health':
return # Skip auth for health checks
auth_header = request.headers.get('Authorization', '')
if not auth_header.startswith('Bearer '):
return jsonify({"error": "unauthorized"}), 401
token = auth_header.split(' ')[1]
try:
jwks = get_jwks()
jwt.decode(
token, jwks,
audience=OKTA_CLIENT_ID,
issuer=OKTA_ISSUER,
algorithms=['RS256']
)
except JWTError:
return jsonify({"error": "invalid token"}), 401
Backend dependencies (add to requirements.txt):
Compliance Mapping¶
| Control | Implementation |
|---|---|
| CIS 5.1 — Account Management | Okta centralizes identity; users provisioned/deprovisioned in one place |
| CIS 5.2 — MFA | Okta enforces MFA policies (configurable per app/group) |
| NIST IA-2 — Identification & Authentication | OIDC provides cryptographic identity verification via signed JWTs |
| NIST IA-5 — Authenticator Management | Okta manages password policies, MFA enrollment, token lifecycle |
| NIST AC-2 — Account Management | Centralized provisioning; disable Okta account = lose access to all portals |
| PCI 8.2 — User Identification | Unique user identity via Okta sub claim; auditable login events |
| PCI 8.3 — MFA | Okta MFA satisfies PCI multi-factor requirement for remote access |
SSO & Identity · Okta Integration · v1.0 · 2026-04-09 · GPUS-IT · Classification: CONFIDENTIAL — Internal Use Only