Skip to content

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.us
  • https://soc.greenpeace.us
  • https://infra.greenpeace.us

Sign-out redirect URIs:

  • https://status.greenpeace.us
  • https://soc.greenpeace.us
  • https://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:

  1. Checks for existing authenticated session
  2. If no session → shows branded login overlay with "Sign in with Okta" button
  3. Redirects to Okta for authentication
  4. Handles the callback (exchanges authorization code for tokens via PKCE)
  5. Extracts user profile (name, email, groups)
  6. 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 /userinfo endpoint
  • 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

  1. Authorization code interception — the code alone is useless without the code_verifier
  2. Cross-site request forgery — state parameter validated on callback
  3. 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 — change OKTA_DOMAIN and CLIENT_ID
  • [ ] Add all portal redirect URIs in production Okta app
  • [ ] Assign users/groups in production Okta
  • [ ] Deploy updated gpus-okta-auth.js to 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):

  1. Frontend sends the ID token (JWT) in Authorization: Bearer <token> header
  2. Backend validates JWT signature against Okta JWKS endpoint (/oauth2/v1/keys)
  3. Backend checks issuer, audience (client_id), and expiry claims
  4. 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):

python-jose[cryptography]>=3.3.0
requests>=2.31.0

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