-
Notifications
You must be signed in to change notification settings - Fork 1
Design and interaction between this extension and a PSSO Extension
This document is an attempt to describe the components of the Keycloak Platform Single Sign-on extension and how it works with a custom PSSO Extension.
Some concepts:
- this extension usually means the Keycloak Platform Single Sign-on extension;
- PSSO Extension and SSO Extension are part of the same package installed on the Mac. The distinction is made as the PSSO Extension is connected to device/user registration, sending a login request and obtaining a login response to the IdP, whether the SSO Extension is used when an authentication attempt is made by an application.
In order to support Platform Single Sign-on, an IdP needs to implement a few services:
- a
nonceendpoint for login requests - a
tokenendpoint for receiving and processing login requests and sending back login responses - device and user registration
- a client used by the PSSO extension.
The nonce and token endpoints need to be implemented according to Apple specifications. The nonce endpoint can be a bit customized, but this Keycloak extension implements it on a default form.
The device and user registration endpoints are custom designed and each IdP implements them as they see fit.
This extension takes into account that a client called psso exists on Keycloak. In the future, we hope that administrators can use the client they want.
Our custom SSO Extension also uses that client during device registration and user registration for user authentication. It doesn't have to be the same client, so in the future we might add the possibility to use different clients for device and user registration and for regular PSSO login requests and responses.
All Endpoints used here are under /realms/<your realm>/psso, with the exception of the endpoint where Keycloak made its public keys available, which is /realms/<your realm>/protocol/openid-connect/certs.
This extension implements the following endpoints:
| Service | Endpoint |
|---|---|
| nonce | /realms/<your realm>/psso/nonce |
| token | /realms/<your realm>/psso/token |
| device registration | /realms/<your realm>/psso/enroll |
| user registration | /realms/<your realm>/psso/userenroll |
All endpoints are implemented using Keycloak's extensible Resource SPI. This extension implementation of the API can be found here.
While the nonce and token endpoints are implemented as per Apple specifications, the other two endpoints were implemented using a few choices and possibilities that Keycloak offers.
The login response includes, among other things, these data:
id_tokenrefresh_tokenexpires_inrefresh_token_expires_in
Apple doesn't document well how the expiration information that should be included with a login response is supposed to be used.
This extension implements it the following way:
When the offline_access scope is present on the client, the Keycloak extension does not include a refresh_token_expires_in information on the login response. Therefore, when this is the case, it is recommended that on the psso client the access token lifespan is configured with a higher value than default, such as 8 hours. This means that the user will need reauthentication every eight hours, which seems to be a reasonable value since most users do lock their computer at least once during a workday, so new tickets are always retrieved anyway whenever the user unlocks his mac after four hours after previous unlocking.
If the offline_access is not present, the extension sets the refresh_token_expires_in to the time the refresh token will expires.
This has some implications on which of these tokens will be used when the SSO Extension performs authentication, as we will see later when we describe this extension's custom Authenticator.
The device registration endpoint requires the following data:
DeviceSigningKey;
DeviceEncryptionKey;
SignKeyID;
EncKeyID;
attestation;
nonce;
accessToken;
The header should contain the client-request-id value.
The keys and their ID's are generated by the PSSO extension. The nonce value is retrieved from this extension's nonce endpoint.
The accessToken here is obtained after the SSO Extension prompts the user to authenticate, which is done by using the idpLogin() function on the extension code.
Ideally, the device registration shouldn't have to prompt the user - it would rather use the RegistrationToken value sent by the MDM. But since not all MDMs support sending a dynamic, verifiable value for this key, we rely on three values to perform a device registration:
- the
attestationtoken - the
accessToken - some check with the MDM to verify if the device exists there. We haven't implemented this yet, and
attestationmight suffice.
The attestation token is sent by the device via its
PSSO Extension. It is checked by Keycloak against Apple Root CA, which is included in the extension. Check the beginDeviceRegistration function on our custom SSO Extension, and the our AppleAttestationVerifier class.
With the attestation, Keycloak gets these:
- confirmation that the keys were generated from a real mac;
- confirmation that the device is enrolled (attestation can only be performed when allowed via MDM profile);
- serial number;
- device UDID.
The device information is persisted in Keycloak by implementing a custom JPA Entity. This allows extending Keycloak's data model. Notice that, because implementing a custom JPA Entity adds a new table on the database, and this requires the implementation of a database changelog, this extension only supports Keycloak instances that use MariaDB or PostgreSQL as their databases. It should be very simple to use other databases, and if you can help us to support others, send a PR for these files with table description for your database model.
The user registration endpoint takes the following data:
attestation;
nonce;
accessToken;
userKey;
userKeyId;
The attestation is the same as the one described above on the device registration topic. However, here it is the userKey that is attested.
The accessToken is also sent by the custom SSO Extension, which actually saves it if user registration is performed right after device registration, in order to avoid authenticating the user twice.
Contrary to the keys used on device registration, the userKey and its ID data are not persisted as a JPA Entity. They are persisted as a credential using Keycloak's extensible CredentialModel class. This means that the user key is saved as a Keycloak credential. This approach makes it possible for the user and the admin to manage the credential on the respective account and admin console.
Apple doesn't really describe how the SSO Extension should authenticate the user to the IdP when the extension intercepts an authentication request.
What it does is to make the loginManager available to the SSO Extension. This makes the tokens received by the PSSO Extension available to the SSO Extension. The loginManager has a ssoTokens object containing:
id_tokenrefresh_tokenexpires_inrefresh_token_expires_in
It is not clear if Apple has any opinion or guidelines on or if these should be used by the SSO Extension, so we had to use some imagination when implementing our authenticator.
A little digression:
The SSO Extension is meant to intercept authentication requests to an IdP so that authentication is centralized: all applications (Safari, native applications, etc) will have authentication to the IdP (and maybe session management by cookies) managed by the SSO Extension. That is its main purpose.
What is very unclear is how (or if) the ssoTokens should be used by the SSO Extension. Those ssoTokens do look like normal OIDC tokens. In the case of this extension, they are normal OIDC tokens. However, this extension here is used to authenticate the user in two particular scenarios:
- the user is authenticating to a SAML application
- the user is authenticating to an OIDC application
In such a flow, a token is not used as part of the authentication process: the application wants a SAML response or a code on a typical OIDC authorization code flow.
A normal flow, without PSSO, would be:
- The SSO Extension intercepts the authentication attempt
- it presents the IdP's page so that the user authenticates
- it redirects back to the application, sending cookies and the responses the client needs.
With PSSO, the SSO Extension can also authenticate the user instead of presenting the IdP's page.
This means that the developer of the IdP is responsible to decide how the SSO Extension will perform user authentication: he can decide to use the tokens under loginManager.ssoTokens, or he can design something purely based on some challenges/signatures. The sky is the limit for how authentication here can occur.
This extension implemented user authentication the following way:
- The SSO Extension creates an envelope (a JSON object) signed by the device key. The envelope contains:
"token"
"token_type"
"kid"
"signed_at"
"username"
"user_kid"
The user_kid value here is used basically to find the user credential, so that we see if the user actually is registered for Platform Single Sign-on on this device. We could perform more checks here, but since there is already a token obtained by the user, it is redundant to check more.
The token sent is either the refresh_token or the id_token. If the refresh_token is an Offline token (which we know when there is no refresh_token_expires_in value on ssoTokens, then we send the id_token. Otherwise, we send the refresh_token.
The signed envelope is then injected as a value for the header with the key Platform-SSO-Authorization.
The custom Keycloak authenticator then:
- inspects the header;
- verifies the envelope signature;
- introspects the token to see if it is valid;
- attaches the user to the session previously created by a login request
If the token has expired or if the application asks for a re-authentication, which is usually one of the three situations:
- A SAML request with
ForceAuthn; - An OIDC request with
prompt=login; - A Keycloak required action.
then the authenticator returns a challenge to the SSO Extension, which then asks the user to authenticate (either via Touch ID or password), and then a new login request is sent, to which Keycloak responds with a fresh login response with new, fresh tokens. The SSO extension then sends a new token back, and the user is authenticated.
The rationale for the authentication and re-authentication are simple:
- the token is user bound, generated for this user, and signed by the device. It is then, after enveloped in a signed JWT, suitable as a credential;
- the re-authentication, triggering the local authentication, seems like a good procedure because the user could generate new tokens simply by locking and unlocking the computer or going to the user panel and click on "Authenticate". So the idea is to facilitate it for the user to obtain new tokens. We could instead ask the user to use password authentication, but it doesn't seem like a good thing to do.
We implemented a few other Keycloak components:
- Required action - a component we used in order to display the user credential on his account console;
- Token validation - some classes that uses different Keycloak functions to validate tokens.