OpenID Connect 1.0 SSO deprecated

Introduction

App Suite acts as OpenID Connect 1.0 Relying Party (RP): users authenticate at an OpenID Provider (OP), and App Suite creates its session from the issued ID token. See the OpenID Connect Core specification for the protocol itself, and the Keycloak setup guide for an example OP.

Not supported: implicit and hybrid flows, the userinfo endpoint (contexts and users are resolved from the ID token only), and the session status iframes.

Feature Overview

  • Web SSO using the authorization code flow, protected by state, nonce and optionally PKCE and pushed authorization requests
  • HTTP API login with username/password using the Resource Owner Password Credentials Grant
  • Direct autologin into an existing App Suite session
  • Logout with or without termination of the OP session
  • Front- and backchannel logout initiated by the OP
  • OAuth token refresh, and token exchange for background tasks
  • Third-party initiated login
  • Several OIDC backends in one instance, e.g. one per host, and several tenants per backend

Message Flows

Conventions of the diagrams:

  • /appsuite/api/ is the dispatcher prefix, OP endpoints use the typical Keycloak paths https://<op>/realms/<realm>/protocol/openid-connect/....
  • Red arrows run through the browser, so their URLs must be reachable by the client. Blue arrows are back-channel calls made by the middleware only.
  • Names in purple monospace are configuration properties com.openexchange.oidc.<name>.

Web SSO Login

Code flow login

com.openexchange.oidc.rpRedirectURIAuth is not an endpoint of the OP, but the authentication callback of App Suite (step 6/7). It has to be registered as valid redirect URI for the client at the OP.

Who calls which configured URL:

Property Called by Must be reachable from
opAuthorizationEndpoint Browser Client
rpRedirectURIAuth Browser (App Suite URL) Client
opTokenEndpoint Middleware: code exchange, token refresh, token exchange, password grant Middleware
opPushedAuthorizationRequestEndpoint Middleware, before redirecting to opAuthorizationEndpoint (only if set) Middleware
opJwkSetEndpoint Middleware Middleware
opLogoutEndpoint Browser; also the middleware with opLogoutOnSessionRemoval or the password grant Client, and the middleware if used that way
rpRedirectURIPostSSOLogout, rpRedirectURILogout Browser (App Suite URLs) Client
<dispatcherPrefix>/oidc/<backendPath>/backchannel_logout OP OP

opIssuer is never called. It is compared with the iss claim of the ID token and has to match it exactly. Keycloak derives iss from its frontend hostname, so if browser and middleware reach the OP under different hostnames, opIssuer usually holds the external one. The issuer in the OP's .well-known/openid-configuration shows the expected value.

Protection of the authentication request:

  • state and nonce are generated per request and remembered for five minutes. A callback is only accepted for a pending state, and the ID token must carry the remembered nonce.
  • bindStateToBrowser additionally sets a short-lived cookie for the state, and only accepts the callback along with it. The cookie is scoped to host and path of rpRedirectURIAuth, so the login has to start on the same host as the callback.
  • pkceMethod S256 enables PKCE (RFC 7636): an intercepted code cannot be redeemed elsewhere. Once enabled, the OP can require PKCE for the client. plain offers no protection and is discouraged.
  • opPushedAuthorizationRequestEndpoint enables pushed authorization requests (RFC 9126): the middleware sends the parameters of the authentication request, including state, nonce and the PKCE challenge, to the OP over the back channel, and steps 2 and 3 only carry client_id and the request_uri returned by the OP. This also covers the silent re-login of App Suite UI. There is no fallback if the push fails; once enabled, the OP can require PAR for the client.

Autologin

Autologin direct

Controlled by com.openexchange.oidc.autologinCookieMode:

Value Behavior
ox_direct (default) As shown: the existing App Suite session found via the open-xchange-session- cookie is used directly, after a forced token refresh. Without cookie or session, or if the refresh fails, the regular login starts.
sso_redirect Deprecated since 7.10.5. Runs the regular login flow: the OP skips its login page while its session is valid (step 4/5), and the existing App Suite session is reused in step 13.
off No autologin.

The regular App Suite autologin is skipped automatically for OIDC sessions, no further configuration is needed.

Logout

Logout via OP

Logout direct

Property Purpose
ssoLogout true: terminate the OP session as well (first diagram). false: log out of App Suite only (second diagram).
rpRedirectURIPostSSOLogout App Suite callback the OP redirects to after its logout. Has to be registered at the OP as valid post-logout redirect URI.
rpRedirectURILogout Final page after the logout, e.g. a custom goodbye page. Also the fallback if the App Suite session no longer exists.

If the logout fails, e.g. due to an invalid response from the OP, the App Suite session is terminated anyway. An OP that shows its own logout confirmation page can send the user back with type=resume to the post-logout callback, which then returns into the session instead of logging out.

OP sessions can also be terminated without user interaction, see Termination of OP Sessions.

Third-Party Initiated Login

A third-party initiated login starts with a GET or POST to <dispatcherPrefix>/oidc/<backendPath>/init?flow=thirdParty:

  • iss is required and must equal opIssuer.
  • target_link_uri is optional and must equal uiWebPath.
  • login_hint is passed on to the OP.

API Login

Clients that only support username and password (OX Mail, Exchange ActiveSync, CalDAV/CardDAV, ...) can still authenticate against the OP via the Resource Owner Password Credentials Grant, enabled with com.openexchange.oidc.enablePasswordGrant. These sessions are not shared SSO sessions, so removing one always logs it out at the OP as well, regardless of ssoLogout.

Token Exchange

Background tasks that act for a user outside of the session, such as scheduled or snoozed mail, can obtain their own tokens through an OAuth token exchange at opTokenEndpoint. See the mail credential vault for configuration.

Tokens and Session Lifetime

The OP must always issue an OAuth access token along with the ID token.

ID, access and refresh token are stored in the App Suite session, and the access token is validated on every request:

  • Expiry: with expires_in, the session is terminated on the first request after expires_in - oauthRefreshTime / 1000 seconds (oauthRefreshTime defaults to 60000 ms).
  • Refresh: with a refresh token, the access token is refreshed at that point instead. An invalid or revoked refresh token, an invalid_grant response or an invalid ID token terminates the session.
  • No expires_in: the session lives as long as the short-term session lifetime, and the access token is never refreshed.

An ID token returned by a refresh must keep iss, sub, aud, auth_time and azp of the original one and carry a new iat, see OpenID Connect Core, section 12.2.

Sessions are not "stay signed in" by default. staySignedIn changes that:

staySignedIn Cookie lifetime Maximum session idle time
false (default) browser session com.openexchange.sessiond.sessionDefaultLifeTime
true com.openexchange.cookie.ttl com.openexchange.sessiond.sessionLongLifeTime

It may help to align the session lifetime with the lifetime of the refresh tokens.

Cookies in use: open-xchange-public-session-, open-xchange-secret- and open-xchange-session-, plus the short-lived open-xchange-oidc-state- while a login is pending with bindStateToBrowser. See Cookies.

Operators Guide

All properties are listed on the property documentation site.

Backends

Backend Enabled by Property names
Default com.openexchange.oidc.startDefaultBackend=true com.openexchange.oidc.<property>
Named com.openexchange.oidc.startOtherConfiguredBackends=true and com.openexchange.oidc.otherConfiguredBackends=<name>,... com.openexchange.oidc.<name>.<property>, see below
Custom an own OIDCBackend implementation, see the Developers Guide as implemented

For a named backend, an unset string property falls back to the default backend's value. Boolean and integer properties, e.g. bindStateToBrowser or tokenLockTimeoutSeconds, do not: they use the property's own default and have to be set per backend.

Every backend needs its own backendPath; its servlets are registered under <dispatcherPrefix>/oidc/<backendPath>/. hosts restricts a backend to a comma-separated list of host names (default all), e.g. to serve different OPs, or different external host names of the same OP, per App Suite host. com.openexchange.oidc.enabled=true is required in any case.

User Resolution

By default, the sub claim is expected as <user-name>@<context-name>, where <context-name> is a login mapping or the numeric identifier of the context. Configurable via:

  • com.openexchange.oidc.contextLookupClaim and com.openexchange.oidc.contextLookupNamePart
  • com.openexchange.oidc.userLookupClaim and com.openexchange.oidc.userLookupNamePart

Name parts are full, local-part and domain. A custom backend can override the resolution.

Web UI

  • The UI is reached at com.openexchange.oidc.uiWebPath, or com.openexchange.UIWebPath (default /appsuite/) if not set.
  • Enable the OIDC login handling of the UI with the oidcLogin flag in the as-config.yml:
default:
    host: my-domain.com
    oidcLogin: true

Front- and Backchannel Logout

For single sign-out initiated by the OP. If either is enabled, a Redis-backed mapping from OIDC to App Suite sessions is maintained.

Frontchannel Backchannel
Specification Front-Channel Logout Back-Channel Logout
Enabled by com.openexchange.oidc.frontchannelLogoutEnabled=true com.openexchange.oidc.backchannelLogoutEnabled=true
Servlet <dispatcherPrefix>/oidc/<backendPath>/frontchannel_logout <dispatcherPrefix>/oidc/<backendPath>/backchannel_logout
Called by OP, via the browser OP, directly
Session reference sid request parameter sid claim of the logout token
At the OP register as "Front-Channel Logout URL" register as "Back-Channel Logout URL", enable "Backchannel Logout Session Required"

Termination of OP Sessions

The OP session is terminated:

  • on a user-initiated logout, if com.openexchange.oidc.ssoLogout=true,
  • always for sessions from the password grant,
  • on further session removals listed in com.openexchange.oidc.opLogoutOnSessionRemoval, by calling opLogoutEndpoint from the middleware without redirects:
    • expired - the session was idle for too long
    • user_closed - the user closed another of their sessions, usually via the session management API
    • admin_closed - an administrator closed the session, e.g. with the closesessions command line tool or the REST API

Example Configuration

Based on the Keycloak setup:

com.openexchange.oidc.enabled=true
com.openexchange.oidc.startDefaultBackend=true

com.openexchange.oidc.hosts=appsuite.example.com

com.openexchange.oidc.ssoLogout=true
com.openexchange.oidc.opLogoutOnSessionRemoval=user_closed,admin_closed

com.openexchange.oidc.clientId=appsuite.example.com
com.openexchange.oidc.clientSecret=vVl8eEWUZOEJ9QZHfOEXAMPLESECRET
com.openexchange.oidc.pkceMethod=S256
com.openexchange.oidc.bindStateToBrowser=true

com.openexchange.oidc.rpRedirectURIAuth=https://appsuite.example.com/appsuite/api/oidc/auth
com.openexchange.oidc.rpRedirectURIPostSSOLogout=https://appsuite.example.com/appsuite/api/oidc/logout
com.openexchange.oidc.rpRedirectURILogout=https://appsuite.example.com/

com.openexchange.oidc.opIssuer=https://keycloak.example.com/realms/demo
com.openexchange.oidc.opAuthorizationEndpoint=https://keycloak.example.com/realms/demo/protocol/openid-connect/auth
com.openexchange.oidc.opTokenEndpoint=https://keycloak.example.com/realms/demo/protocol/openid-connect/token
com.openexchange.oidc.opJwkSetEndpoint=https://keycloak.example.com/realms/demo/protocol/openid-connect/certs
com.openexchange.oidc.opLogoutEndpoint=https://keycloak.example.com/realms/demo/protocol/openid-connect/logout

com.openexchange.oidc.failureRedirect=https://www.example.com/

com.openexchange.oidc.enablePasswordGrant=true
com.openexchange.oidc.passwordGrantUserNamePart=local-part

com.openexchange.oidc.contextLookupClaim=email
com.openexchange.oidc.userLookupClaim=email

With email as lookup claim, the e-mail address has to be included in the ID token: check Add to ID token in the client's email attribute mapper.

Developers Guide

Bundle Content
com.openexchange.oidc Interfaces, e.g. OIDCBackend, OIDCBackendConfig
com.openexchange.oidc.impl Default implementation, based on the Nimbus OAuth 2.0 / OpenID Connect SDK
com.nimbus The Nimbus SDK

To implement a custom backend:

  • Extend com.openexchange.oidc.spi.AbstractOIDCBackend and override only what has to behave differently, e.g. the user resolution.
  • Extend com.openexchange.oidc.spi.AbstractOIDCBackendConfig for its configuration and override only the differing properties, so backends can share the rest.
  • Register it as OIDCBackend OSGi service; it is picked up like the default backend.
  • Check com.openexchange.oidc.tools.OIDCTools for helpers before writing your own.