Skip to main content

SSO & OIDC Integration

Openlane provides comprehensive Single Sign-On (SSO) integration through OpenID Connect (OIDC) standards, enabling organizations to enforce authentication through their preferred identity providers. The system supports major enterprise providers including Okta, Google Workspace, OneLogin, and any OIDC-compliant provider.

SSO Architecture Overview

OIDC Authentication Flow

Security Model

The SSO integration implements enterprise-grade security:

  • State Parameter Validation - CSRF protection using cryptographically secure state
  • Nonce Binding - Replay attack prevention with unique nonce values
  • Token Validation - Full OIDC ID token signature and claims validation
  • Session Management - Secure session creation with proper cookie settings
  • Organization Isolation - SSO configuration scoped to individual organizations

Supported Identity Providers

Okta Integration

  1. Create Application in Okta:

    • Navigate to Applications → Create App Integration
    • Select OIDC - Web Application
    • Configure redirect URI: https://your-domain.com/oidc/callback
  2. Configuration Values:

    # Okta configuration
    Client ID: 0oa1234567890abcdef
    Client Secret: your-client-secret
    Issuer URI: https://dev-123456.okta.com/oauth2/default
    Discovery Endpoint: https://dev-123456.okta.com/oauth2/default/.well-known/openid-configuration
  3. Required Scopes:

    • openid (required for OIDC)
    • profile (user profile information)
    • email (user email address)

Google Workspace Integration

  1. Create OAuth Application:

    • Go to Google Cloud Console
    • Navigate to APIs & Services → Credentials
    • Create OAuth 2.0 Client ID for web application
    • Add authorized redirect URI: https://your-domain.com/oidc/callback
  2. Configuration Values:

    # Google Workspace configuration
    Client ID: 123456789-abcdef.apps.googleusercontent.com
    Client Secret: your-google-client-secret
    Discovery Endpoint: https://accounts.google.com
  3. Domain Restriction (Optional): Configure Google Workspace to restrict sign-ins to your organization's domain.

Custom OIDC Providers

For any OIDC-compliant provider:

mutation UpdateOrgSSO {
updateOrganizationSetting(
id: "org-setting-id"
input: {
identityProvider: OIDC
identityProviderClientID: "your-client-id"
identityProviderClientSecret: "your-client-secret"
oidcDiscoveryEndpoint: "https://your-provider.com/auth/realms/your-realm"
identityProviderLoginEnforced: true
}
) {
organizationSetting {
id
identityProvider
identityProviderLoginEnforced
}
}
}

SSO Implementation Details

OIDC Configuration Builder

// Build OIDC relying party configuration
func (h *Handler) oidcConfig(ctx context.Context, orgID string) (*rp.RelyingParty, error) {
// Fetch organization SSO settings
orgSetting, err := h.getOrgSetting(ctx, orgID)
if err != nil {
return nil, err
}

// Validate SSO configuration
if !orgSetting.HasValidSSO() {
return nil, ErrInvalidSSOConfig
}

// Build OIDC configuration
config := &oidc.Config{
ClientID: orgSetting.IdentityProviderClientID,
ClientSecret: orgSetting.IdentityProviderClientSecret,
RedirectURI: h.buildRedirectURI(),
Scopes: []string{"openid", "profile", "email"},
}

// Create relying party with discovery
return rp.NewRelyingPartyOIDC(
ctx,
orgSetting.OIDCDiscoveryEndpoint,
config.ClientID,
config.ClientSecret,
config.RedirectURI,
config.Scopes,
)
}

State and Nonce Management

// Generate secure state and nonce for OIDC flow
func (h *Handler) SSOLoginHandler(ctx echo.Context) error {
orgID := getOrgIDFromRequest(ctx)

// Generate cryptographically secure values
state := ulids.New().String()
nonce := ulids.New().String()

// Set secure cookies for validation
cfg := sessions.CookieConfig{
Path: "/",
HTTPOnly: true,
SameSite: http.SameSiteLaxMode,
Secure: !h.IsTest,
}

sessions.SetCookie(ctx.Response().Writer, state, "state", cfg)
sessions.SetCookie(ctx.Response().Writer, nonce, "nonce", cfg)
sessions.SetCookie(ctx.Response().Writer, orgID, "organization_id", cfg)

// Build authorization URL
rpCfg, err := h.oidcConfig(ctx.Request().Context(), orgID)
if err != nil {
return h.BadRequest(ctx, err)
}

authURL := rpCfg.OAuthConfig().AuthCodeURL(state,
oauth2.SetAuthURLParam("nonce", nonce))

return ctx.Redirect(http.StatusFound, authURL)
}

Token Validation and User Provisioning

// Handle OIDC callback and provision user
func (h *Handler) SSOCallbackHandler(ctx echo.Context) error {
reqCtx := ctx.Request().Context()

// Validate state and nonce
if err := h.validateOIDCState(ctx); err != nil {
return h.BadRequest(ctx, err)
}

// Get organization configuration
orgID := getOrgIDFromCookie(ctx)
rpCfg, err := h.oidcConfig(reqCtx, orgID)
if err != nil {
return h.BadRequest(ctx, err)
}

// Exchange code for tokens
tokens, err := rpCfg.CodeExchange(reqCtx, getCodeFromRequest(ctx))
if err != nil {
return h.BadRequest(ctx, err)
}

// Validate ID token
idTokenClaims, err := rpCfg.IDTokenVerifier().Verify(reqCtx, tokens.IDToken)
if err != nil {
return h.BadRequest(ctx, err)
}

// Extract user information
userInfo := &SSOUserInfo{
Subject: idTokenClaims.Subject,
Email: idTokenClaims.Email,
Name: idTokenClaims.Name,
GivenName: idTokenClaims.GivenName,
FamilyName: idTokenClaims.FamilyName,
Provider: getProviderFromOrgSetting(orgSetting),
}

// Provision or update user
user, err := h.provisionSSOUser(reqCtx, userInfo, orgID)
if err != nil {
return h.InternalServerError(ctx, err)
}

// Create Openlane session
return h.createAuthenticatedSession(ctx, user)
}

User Provisioning and Management

Automatic User Provisioning

// Provision user from SSO claims
func (h *Handler) provisionSSOUser(ctx context.Context, userInfo *SSOUserInfo, orgID string) (*ent.User, error) {
// Check if user already exists
user, err := h.DBClient.User.Query().
Where(user.Email(userInfo.Email)).
First(ctx)

if err != nil && !ent.IsNotFound(err) {
return nil, err
}

if user != nil {
// Update existing user
return h.updateUserFromSSO(ctx, user, userInfo)
}

// Create new user
return h.createUserFromSSO(ctx, userInfo, orgID)
}

func (h *Handler) createUserFromSSO(ctx context.Context, userInfo *SSOUserInfo, orgID string) (*ent.User, error) {
// Create user with SSO claims
user, err := h.DBClient.User.Create().
SetEmail(userInfo.Email).
SetFirstName(userInfo.GivenName).
SetLastName(userInfo.FamilyName).
SetDisplayName(userInfo.Name).
SetEmailVerified(true). // SSO users are pre-verified
SetAuthProvider(enums.AuthProviderOIDC).
Save(ctx)
if err != nil {
return nil, err
}

// Add user to organization
_, err = h.DBClient.OrgMembership.Create().
SetUserID(user.ID).
SetOrganizationID(orgID).
SetRole(enums.RoleMember). // Default role
Save(ctx)
if err != nil {
return nil, err
}

return user, nil
}

Role Mapping and Claims

// Map OIDC claims to Openlane roles
type SSOUserInfo struct {
Subject string `json:"sub"`
Email string `json:"email"`
Name string `json:"name"`
GivenName string `json:"given_name"`
FamilyName string `json:"family_name"`
Groups []string `json:"groups,omitempty"`
Roles []string `json:"roles,omitempty"`
}

func (h *Handler) mapSSORole(userInfo *SSOUserInfo, orgSetting *ent.OrganizationSetting) enums.Role {
// Default role
defaultRole := enums.RoleMember

// Check for admin groups/roles
for _, group := range userInfo.Groups {
if group == orgSetting.AdminGroup {
return enums.RoleAdmin
}
}

// Check for owner roles
for _, role := range userInfo.Roles {
if role == "owner" || role == "admin" {
return enums.RoleOwner
}
}

return defaultRole
}

Security Features

Session Security

// Create authenticated session with proper security
func (h *Handler) createAuthenticatedSession(ctx echo.Context, user *ent.User) error {
// Set authenticated context
userCtx := setAuthenticatedContext(ctx.Request().Context(), user)

// Generate session tokens
accessToken, refreshToken, err := h.TokenManager.CreateTokenPair(userCtx, user.ID)
if err != nil {
return err
}

// Set secure cookies
secureCookieConfig := sessions.CookieConfig{
Path: "/",
HTTPOnly: true,
SameSite: http.SameSiteLaxMode,
Secure: !h.IsTest,
MaxAge: int(h.SessionConfig.AccessTokenTTL.Seconds()),
}

sessions.SetCookie(ctx.Response().Writer, accessToken, "access_token", secureCookieConfig)
sessions.SetCookie(ctx.Response().Writer, refreshToken, "refresh_token", secureCookieConfig)

// Clear SSO flow cookies
h.clearSSOCookies(ctx)

// Redirect to application
returnURL := getReturnURLFromCookie(ctx)
if returnURL == "" {
returnURL = "/dashboard"
}

return ctx.Redirect(http.StatusFound, returnURL)
}

CSRF Protection

// Validate OIDC state parameter to prevent CSRF
func (h *Handler) validateOIDCState(ctx echo.Context) error {
// Get state from request
requestState := ctx.QueryParam("state")
if requestState == "" {
return ErrMissingState
}

// Get state from cookie
stateCookie, err := sessions.GetCookie(ctx.Request(), "state")
if err != nil {
return ErrMissingStateCookie
}

// Validate state matches
if requestState != stateCookie.Value {
return ErrInvalidState
}

// Validate nonce if present
if nonceCookie, err := sessions.GetCookie(ctx.Request(), "nonce"); err == nil {
// Nonce validation would be done during token verification
log.Debug().Str("nonce", nonceCookie.Value).Msg("validating nonce")
}

return nil
}

Organization Enforcement

// Check if SSO is enforced for organization
func (h *Handler) ssoOrgForUser(ctx context.Context, email string) (string, bool) {
// Find user's organization with SSO enforcement
user, err := h.DBClient.User.Query().
Where(user.Email(email)).
WithOrgMemberships(func(q *ent.OrgMembershipQuery) {
q.WithOrganization(func(oq *ent.OrganizationQuery) {
oq.WithSetting()
})
}).
First(ctx)

if err != nil {
return "", false
}

// Check each organization for SSO enforcement
for _, membership := range user.Edges.OrgMemberships {
org := membership.Edges.Organization
if org.Edges.Setting != nil &&
org.Edges.Setting.IdentityProviderLoginEnforced {
return org.ID, true
}
}

return "", false
}

Configuration Examples

Helm Chart Configuration

openlane:
coreConfiguration:
auth:
enabled: true
providers:
# Social login still available for non-SSO orgs
google:
clientId: "social-google-client-id"
clientSecret: "social-google-secret"
github:
clientId: "social-github-client-id"
clientSecret: "social-github-secret"

# Organization-specific SSO configured via GraphQL API
# after deployment, not in Helm chart

Organization-Specific Configuration

# Query current SSO configuration
query GetOrgSSO($orgId: ID!) {
organizationSetting(id: $orgId) {
id
identityProvider
identityProviderClientID
oidcDiscoveryEndpoint
identityProviderLoginEnforced
}
}

# Update SSO configuration
mutation UpdateSSO($orgId: ID!, $config: UpdateOrganizationSettingInput!) {
updateOrganizationSetting(id: $orgId, input: $config) {
organizationSetting {
id
identityProvider
identityProviderLoginEnforced
}
}
}

Testing and Validation

SSO Flow Testing

func TestSSOFlow(t *testing.T) {
// Create test organization with SSO
org := createTestOrgWithSSO(t)

// Start SSO login
resp := testClient.GET("/v1/sso/login?organization_id=" + org.ID)
assert.Equal(t, http.StatusFound, resp.StatusCode)

// Verify redirect to IdP
location := resp.Header.Get("Location")
assert.Contains(t, location, "accounts.google.com")
assert.Contains(t, location, "state=")
assert.Contains(t, location, "nonce=")

// Extract state and simulate callback
state := extractStateFromURL(location)
callbackResp := testClient.GET("/oidc/callback?code=test-code&state=" + state)
assert.Equal(t, http.StatusFound, callbackResp.StatusCode)
}

Integration Testing

# Test OIDC discovery endpoints
curl -s https://accounts.google.com/.well-known/openid-configuration | jq .issuer
curl -s https://dev-123456.okta.com/oauth2/default/.well-known/openid-configuration | jq .issuer

# Validate JWT tokens
echo $ID_TOKEN | jwt decode --no-verify

# Test SSO enforcement
curl -i https://api.theopenlane.io/v1/sso/login?organization_id=test-org

Monitoring and Troubleshooting

SSO Metrics

// SSO-specific metrics
var (
ssoLoginAttemptsTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "sso_login_attempts_total",
Help: "Total SSO login attempts",
},
[]string{"provider", "org_id", "status"},
)

ssoTokenValidationDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "sso_token_validation_duration_seconds",
Help: "SSO token validation latency",
},
[]string{"provider"},
)
)

Common Issues and Solutions

# Problem: OIDC discovery fails
# Solution: Verify endpoint accessibility
curl -v https://your-idp.com/.well-known/openid-configuration

# Check network connectivity
nslookup your-idp.com
telnet your-idp.com 443

# Verify TLS certificate
openssl s_client -connect your-idp.com:443 -servername your-idp.com

Debugging Commands

# View SSO configuration for organization
kubectl exec -it openlane-pod -- /openlane org-setting get --org-id=ORG_ID

# Check OIDC token validation
kubectl logs openlane-pod | grep "token validation"

# Monitor SSO flows
kubectl logs -f openlane-pod | grep "sso"

# Test organization SSO enforcement
curl -H "Authorization: Bearer $TOKEN" \
https://api.theopenlane.io/query \
-d '{"query": "{ organization(id: \"ORG_ID\") { setting { identityProviderLoginEnforced } } }"}'

This SSO integration provides enterprise-grade authentication while maintaining security, usability, and operational excellence across all supported identity providers.