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,nonceand 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 pathshttps://<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
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:
stateandnonceare generated per request and remembered for five minutes. A callback is only accepted for a pendingstate, and the ID token must carry the rememberednonce.bindStateToBrowseradditionally sets a short-lived cookie for thestate, and only accepts the callback along with it. The cookie is scoped to host and path ofrpRedirectURIAuth, so the login has to start on the same host as the callback.pkceMethodS256enables PKCE (RFC 7636): an intercepted code cannot be redeemed elsewhere. Once enabled, the OP can require PKCE for the client.plainoffers no protection and is discouraged.opPushedAuthorizationRequestEndpointenables pushed authorization requests (RFC 9126): the middleware sends the parameters of the authentication request, includingstate,nonceand the PKCE challenge, to the OP over the back channel, and steps 2 and 3 only carryclient_idand therequest_urireturned 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
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
| 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:
issis required and must equalopIssuer.target_link_uriis optional and must equaluiWebPath.login_hintis 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 afterexpires_in - oauthRefreshTime / 1000seconds (oauthRefreshTimedefaults 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_grantresponse 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.contextLookupClaimandcom.openexchange.oidc.contextLookupNamePartcom.openexchange.oidc.userLookupClaimandcom.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, orcom.openexchange.UIWebPath(default/appsuite/) if not set. - Enable the OIDC login handling of the UI with the
oidcLoginflag in theas-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 callingopLogoutEndpointfrom the middleware without redirects:expired- the session was idle for too longuser_closed- the user closed another of their sessions, usually via the session management APIadmin_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.AbstractOIDCBackendand override only what has to behave differently, e.g. the user resolution. - Extend
com.openexchange.oidc.spi.AbstractOIDCBackendConfigfor its configuration and override only the differing properties, so backends can share the rest. - Register it as
OIDCBackendOSGi service; it is picked up like the default backend. - Check
com.openexchange.oidc.tools.OIDCToolsfor helpers before writing your own.