|
| 1 | +- [OIDC Native SSO](#oidc-native-sso) |
| 2 | + * [Key points to note](#key-points-to-note) |
| 3 | + * [Changes on Client](#changes-on-client) |
| 4 | + * [Changes on OfflineGrant](#changes-on-offlinegrant) |
| 5 | + * [Changes on Authorization Endpoint](#changes-on-authorization-endpoint) |
| 6 | + * [Changes on Token Endpoint](#changes-on-token-endpoint) |
| 7 | + + [`grant_type=authorization_code`](#grant_typeauthorization_code) |
| 8 | + + [`grant_type=urn:authgear:params:oauth:grant-type:biometric-request`](#grant_typeurnauthgearparamsoauthgrant-typebiometric-request) |
| 9 | + + [`grant_type=refresh_token`](#grant_typerefresh_token) |
| 10 | + + [`grant_type=urn:authgear:params:oauth:grant-type:app2app`](#grant_typeurnauthgearparamsoauthgrant-typeapp2app) |
| 11 | + + [`grant_type=urn:ietf:params:oauth:grant-type:token-exchange`](#grant_typeurnietfparamsoauthgrant-typetoken-exchange) |
| 12 | + + [When issuing ID tokens](#when-issuing-id-tokens) |
| 13 | + * [Changes on Admin API](#changes-on-admin-api) |
| 14 | + * [Changes on SDK](#changes-on-sdk) |
| 15 | + + [Recipe: Two applications written by the same vendor](#recipe-two-applications-written-by-the-same-vendor) |
| 16 | + + [Recipe: A application opening webapps with custom webview](#recipe-a-application-opening-webapps-with-custom-webview) |
| 17 | + * [Caveats](#caveats) |
| 18 | + * [Security Considerations](#security-considerations) |
| 19 | + + [Binding Tokens To Device](#binding-tokens-to-device) |
| 20 | + |
| 21 | +# OIDC Native SSO |
| 22 | + |
| 23 | +This document specifies the implementation of [OIDC Native SSO](https://openid.net/specs/openid-connect-native-sso-1_0.html). |
| 24 | + |
| 25 | +## Key points to note |
| 26 | + |
| 27 | +- A OfflineGrant used to has only one set of client-specific information, like `client_id` and `refresh_token`. Now, a OfflineGrant can have multiple refresh tokens. |
| 28 | +- Since there is only one OfflineGrant, the apps sharing user authentication with Native SSO does not share refresh token. But the underlying session is shared. Thus signing out in one app will sign out all apps. This is by design. See [App2app](./app2app.md) if you want the apps have independent sessions. |
| 29 | +- Native SSO is done through the Token Endpoint, thus it requires no user interaction. |
| 30 | +- Existing OfflineGrant cannot perform Native SSO. It is because `device_sso` is not in the `scope`. Sign in again to obtain a Native SSO OfflineGrant to perform Native SSO. |
| 31 | + |
| 32 | +## Changes on Client |
| 33 | + |
| 34 | +- Add `x_device_sso_enabled: boolean`. |
| 35 | +- `scope=device_sso` is allowed if `x_device_sso_enabled=true`. |
| 36 | + |
| 37 | +> Do we need to add `x_device_sso_key: string` to designate which group of clients can perform Native SSO? |
| 38 | +> Only clients with `x_device_sso_enabled=true` AND the same value of `x_device_sso_key` can perform Native SSO with each other. |
| 39 | +> This seems very advanced to me. |
| 40 | +
|
| 41 | +## Changes on OfflineGrant |
| 42 | + |
| 43 | +- The following fields become client-specific |
| 44 | + - `ClientID` |
| 45 | + - `AuthorizationID` |
| 46 | + - `CreatedAt` |
| 47 | + - `Scopes` |
| 48 | + - `TokenHash` |
| 49 | +- Add a new field `RefreshTokens`. It will store all of the above imformation per refresh token. |
| 50 | +- Add a new field `DeviceSecretHash`. It is the hex of SHA256 of `device_secret`. |
| 51 | +- All new offline grants will use the new `RefreshTokens` to store information about refresh tokens. |
| 52 | + |
| 53 | +## Changes on Authorization Endpoint |
| 54 | + |
| 55 | +- Allow `scope=device_sso` if the client has `x_device_sso_enabled=true`. |
| 56 | + |
| 57 | +## Changes on Token Endpoint |
| 58 | + |
| 59 | +### `grant_type=authorization_code` |
| 60 | + |
| 61 | +- If `authorization_code.scope=device_sso` and `device_secret` is present and it is valid, a new `refresh_token` is added to Native SSO offline grant. |
| 62 | +- If `authorization_code.scope=device_sso` and `device_secret` is absent or it is invalid, a new Native SSO offline grant is created. |
| 63 | + |
| 64 | +### `grant_type=refresh_token` |
| 65 | + |
| 66 | +- If `refresh_token.scope=device_sso` and `device_secret` is present and it is valid, nothing to do. |
| 67 | +- If `refresh_token.scope=device_sso` and `device_secret` is absent or it is invalid, a new `device_secret` is generated. `DeviceSecretHash` is updated. |
| 68 | + |
| 69 | +### `grant_type=urn:authgear:params:oauth:grant-type:biometric-request` |
| 70 | + |
| 71 | +- Support a new parameter `scope`. |
| 72 | +- Allow `scope=device_sso` if the client has `x_device_sso_enabled=true`. |
| 73 | +- If `scope=device_sso` and `device_secret` is present and it is valid, a new `refresh_token` is added to Native SSO offline grant. |
| 74 | +- If `scope=device_sso` and `device_secret` is absent or it is invalid, a new Native SSO offline grant is created. |
| 75 | + |
| 76 | +### `grant_type=urn:authgear:params:oauth:grant-type:app2app` |
| 77 | + |
| 78 | +Native SSO has no direct impact on app2app. |
| 79 | + |
| 80 | +### `grant_type=urn:ietf:params:oauth:grant-type:token-exchange` |
| 81 | + |
| 82 | +- Validate `scope=device_sso`. Return `error=invalid_request` otherwise. This is because this is the only Token Change flow we support. |
| 83 | +- Validate `audience` is the origin of the endpoint. |
| 84 | +- Validate `subject_token` is a valid ID token issued to the first app. An expired ID token is still valid. (4.3 Point 2) |
| 85 | +- Validate `subject_token_type` is `urn:ietf:params:oauth:token-type:id_token`. |
| 86 | +- Validate `actor_token` is a valid `device_secret`. (4.3 Point 1) |
| 87 | +- Validate `actor_token_type` is `urn:x-oath:params:oauth:token-type:device-secret`. |
| 88 | +- Validate `requested_token_type` is absent. |
| 89 | +- Validate `subject_token.ds_hash` is the hex of SHA256 of `actor_token`. (4.3 Point 3) |
| 90 | +- Validate `subject_token.sid` is pointing to a valid session. (4.3 Point 4) |
| 91 | +- Validate `client_id` and `subject_token.aud` are allowed to perform Native SSO. (4.3 Point 5) |
| 92 | +- Validate `scope` is equal or a subset of the `scope` in all refresh tokens of the Native SSO offline grant. (4.3 Point 6) |
| 93 | + |
| 94 | +### When issuing ID tokens |
| 95 | + |
| 96 | +- If the offline grant has `DeviceSecretHash`, set `ds_hash` in the ID token. |
| 97 | +- Keep setting `sid`. |
| 98 | + |
| 99 | +## Changes on Admin API |
| 100 | + |
| 101 | +- Add `clientIDs` to `Session`. |
| 102 | +- `Session.clientID` is the first client ID of a Native SSO offline grant. |
| 103 | + |
| 104 | +## Changes on SDK |
| 105 | + |
| 106 | +- Rename `isSSOEnabled` to `isBrowserSSOEnabled` in `ConfigureOptions`. |
| 107 | +- Add `isDeviceSSOEnabled` to `ConfigureOptions`. |
| 108 | +- If `isDeviceSSOEnabled` is true, then `device_sso` is included in authorization requests. |
| 109 | +- Add `deviceSecretStore`. It is responsible for storing `device_secret` and `id_token` in Token Response. |
| 110 | +- If `device_secret` is found in `deviceSecretStore` and `isDeviceSSOEnabled` is true, then `device_secret` is included in Token Request. |
| 111 | +- If `id_token` is present in Token Response, it is persisted into `deviceSecretStore`. |
| 112 | +- If `device_secret` is present in Token Response, it is persisted into `deviceSecretStore`. |
| 113 | +- `logout()` clears `deviceSecretStore`. |
| 114 | +- Add `checkDeviceSSOPossible(): Promise<void>`. It throws error if either `device_secret` or `id_token` is not found in `deviceSecretStore`. |
| 115 | +- Add `authenticateDeviceSSO(): Promise<UserInfo>`. |
| 116 | +- Expose `refreshToken` on Authgear. |
| 117 | + |
| 118 | +Future works |
| 119 | +- Add `IOSAppGroupDeviceSecretStorage`. |
| 120 | +- Add `AndroidAccountManagerDeviceSecretStorage`. |
| 121 | + |
| 122 | +### Recipe: Two applications written by the same vendor |
| 123 | + |
| 124 | +> Recipe requires Future works to be done first. |
| 125 | +
|
| 126 | +1. Configure both apps to use `IOSAppGroupDeviceSecretStorage` and `AndroidAccountManagerDeviceSecretStorage`. |
| 127 | +2. Configure both apps to set `isDeviceSSOEnabled` to true. |
| 128 | +3. Sign in normally in App 1. |
| 129 | +4. In App 2, call `checkDeviceSSOPossible()`. It returns normally. |
| 130 | +5. In App 2, if `checkDeviceSSOPossible()` returns normally, call `authenticateDeviceSSO()`. |
| 131 | +6. In App 2, the end-user is authenticated. No user interaction is involved. |
| 132 | + |
| 133 | +### Recipe: A application opening webapps with custom webview |
| 134 | + |
| 135 | +1. Configure the app to set `isDeviceSSOEnabled` to true. |
| 136 | +2. Sign in normally. |
| 137 | +3. To open a webapp, do the following |
| 138 | +4. Construct a new Container with `tokenStorage` set to TransientStorage, and `isDeviceSSOEnabled` to true. The purpose of this is to prevent this Container from messing with the original Container. The new Container is now unauthenticated (due to TransientStorage) but has `device_secret` (due to `isDeviceSSOEnabled` being true). |
| 139 | +5. Call `authenticateDeviceSSO()` |
| 140 | +6. Inject `refreshToken` of the new Container into the custom webview. This requires knowledge on the implementation details of the Web SDK. |
| 141 | +7. The Container of the Web SDK considered itself as authenticated due to the injected refresh token. |
| 142 | + |
| 143 | +## Caveats |
| 144 | + |
| 145 | +- The iOS keychain will not be cleaned up even all apps in the app group were removed. |
| 146 | +- Developer is required to provide the `accountType` to initialize the store. Applications must belong to the given app group or the SDK might malfunction. Developer must also define a `<account-authenticator>` resource with the same `accountType`, and a `<service>` which uses the defined account authenticator in all apps that is sharing the authentication session. For details read [this document](https://developer.android.com/reference/android/accounts/AbstractAccountAuthenticator). |
| 147 | +- The first installed app will be the "authenticator" app in the android, which in fact owns the accounts. Once the app was removed, the accounts will be removed together with the app. |
| 148 | + |
| 149 | +## Security Considerations |
| 150 | + |
| 151 | +### Binding Tokens To Device |
| 152 | + |
| 153 | +device_secret should always be bound to a device. |
| 154 | + |
| 155 | +[OAuth 2.0 Demonstrating Proof of Possession](https://datatracker.ietf.org/doc/html/rfc9449) is implemented for such binding. |
| 156 | + |
| 157 | +The DPoP Proof MAY be provided when making requests to the /token endpoint. If DPoP Proof is provided, device_secret returned in the response will be bound to the public key provided in the DPoP Proof. Which the keypair used to sign such DPoP Proof is expected to be stored in a secure device storage such as iOS Keychain or Android Keystore. |
0 commit comments