# AssureQA idP integration
## Auth[nz] flow
### Auth guard
- **User**: Visits a protected route: `#/protected/page`
- `if isAuthenticated()`
```bash
- if valid access token available → authenticated=true
- if it could get access token using refresh token → authenticated=true
- else → authenticated=false
```
- `if isAuthorized()` *(access control information is available in access token payload)*
- **Browser**: Renders page
- `if not isAuthorized()`
- **Browser**: Flashes a warning OR redirects to fallback/error page
- `if not isAuthenticated()`
- **Browser**: Navigates to login page preserving current route in `redirectTo` query parameter eg `#/login.html?redirectTo=%2Foriginal%2Fpage` (query parameter should be urlencoded)
### Login page
- **Browser**: Asks backends about available auth options. `GET /api/auth/options`
- **Browser**: Renders `#/login.html?re
To=%2Foriginal%2Fpage` with different login options
### Direct: username/password input form
#### login
- **User**: Fills form and clicks login button
- **Browser**: Sends XHR POST with `{username,password}` to JSON API `/api/auth/login`
- **Backend**:
- Validates user input
- Creates access-token and refresh-token (htmlonly cookie) containing user attributes and authorization roles and idpType=null
- **Browser**:
- Receives response [200]
- Puts access-token in LocalStorage, adds refresh token to cookie jar.
- Navigates to `#/protected/page`
#### logout
- **User**: Clicks logout button
- **Browser**: `if user.idpType === null`
- Removes access token from localStorage
- XHR POST to `/api/auth/logout`
- After XHR completion, navigates to home/login page
- **Backend**: Removes refresh-token cookie
### SSO with OIDC idP
> !! Out of scope.
### SSO with SAML idP
#### login
- **User**: Chooses an idP option by clicking `Login with <idpDisplayName>` button
- **Browser**: Navigates to `/sso/saml/<idp-uuid>/sp-init-sso-redirect-binding?redirectTo=%2Foriginal%2Fpage` (HTTP GET to backend)
- **Backend**:
- Receives request at `/sso/saml/<idp-uuid>/sp-init-sso-redirect-binding`
- Gets idP configuration for given provider from DB
- Creates SAML SP-Init request HTTP-Redirect binding url with RelayState containing query parameters sent from Browser
- Responds with SP-Init AuthnRequest url as location redirect and statusCode 302/303
- **Browser**: Follows location url and redirects to idP login page
- **User**: Completes login flow presented by idP
- **Browser**: Submits HTTP POST to AssertionConsumerService endpoint `/sso/saml/<idp-uuid>/acs` (Contains SAML AuthnResponse with Original Relay state)
- **Backend**:
- Receives request at `/sso/saml/<idp-uuid>/acs`
- Gets idP configuration for given provider from DB
- Validates SAML response
- Gets user info from response (Maps SAML attributes to application user attributes )
- Upsert user
- Infers additional JWT user claims for SAML. { idpType: 'saml', idpId, samlNameId: nameId, samlSloEnabled }
- Infers JWT token expiry from SAML response
- Creates JWT refresh token and set as htmlOnly Cookie (Payload should contain `{idpType:"saml",idpId:"xxxx"}` fields)
- Gets redirectTo path from SAML RelayState
- Responds with location redirection (statusCode must be 303) to `<redirectTo>` (ie: `/original/page`)
- Browser: Requests access token with the help of refresh token. (Payload should contain `{idpType:"saml",idpId:"xxxx"}` fields)
#### logout (SP initiated)
- **User**: Clicks logout button within Application
- **Browser**:
- `if user.idpType === 'saml' && user.samlSloEnabled`
- Navigates to `/sso/saml/<idp-uuid>/sp-init-logout-redirect-binding?redirectTo=%2Fauto-logout` redirectTo parameter is set to APP_AUTO_LOGOUT_ROUTE route implemented in frontend
- `if user.idpType === 'saml' && !user.samlSloEnabled`
- Follows the same logout flow as in direct username/password authentication/
- **Backend**:
- Receives request at `/sso/saml/<idp-uuid>/sp-init-logout-redirect-binding`
- Gets idP configuration for given provider from DB
- Creates SAML logout request HTTP-Redirect binding url with RelayState containing query parameters sent from Browser
- Responds with location redirection (statusCode must be 303)
- **Browser**:
- Redirects to idP logout page which logs user out from idP side,
- Issues HTTP GET/POST request to SingleLogoutService endpoint `/sso/saml/<idp-uuid>/slo` (Contains SAML Logout response with Original Relay state)
- **Backend**:
- Receives request at `/sso/saml/<idp-uuid>/slo`
- Gets idP configuration for given provider from DB
- Parses and validates response
- Clears refresh-token cookie
- Gets redirectTo value from RelayState (this will be to the auto-logout route implemented in the frontend)
- Responds with location redirection (statusCode must be 303) to `<redirectTo>` (ie: `/auth/login`)
- **Browser**:
- Routes to APP_AUTO_LOGOUT_ROUTE which mounts AutoLogout component
- Follows the normal logout flow as in direct username/password authentication/
#### logout (IdP initiated)
- **User**: Clicks logout button in IdP console
- **Browser**: Issues HTTP GET/POST request to SingleLogoutService endpoint `/sso/saml/<idp-uuid>/slo` (Contains SAML Logout request)
- **Backend**:
- Receives request at `/sso/saml/<idp-uuid>/slo`
// Further steps are same as above.
## Application features
### Frontend
#### Login page
- Calls `/api/auth/options` to get available authentication options and render login page accordingly.
- Username/password form is not rendered if `isPasswordLoginDisabled===true`
- For each idPs, render a button with given displayName and icon. Button label could `Login with <displayName>` and provide contextHelp content on hover.
#### New react route: APP_AUTO_LOGOUT_ROUTE
- preferable path: `/auto-logout`
- An `AutoLogout` component will be mounted at above path, which does the normal logout steps upon loading.
- So when browser is navigated to this route, user is logged out automatically. i.e, no explicit user action is required.
#### Plugin page
- New plugin: "SSO integration"
- Plugin landing page lists existing idPs using `GET /api/idp` as a list of cards.
- Clicking on a specific idP card, should open idP specific configuration view containing
- Update button -> render updatable form with all configuration fields
- Remove button -> deletes idP
- Render existing configuration (in panel to the left side)
- Show sp-metadata content (in panel to the right side) with "Copy"/"Save as file" buttons
- In plugin landing page. there should be an option to add new idP
- If at least one IdP is created, provide an option in plugin landing page to set 'isPasswordLoginDisabled' (NOTE: this should be passed to the backend though a different settings api, as this is not part of the idP CRUD operations)
- IdP Add/Update option renders a configuration form with following fields and submits to `POST /api/idp` (for Add) / `PUT /api/idp/<idp-uuid>` (for Update)
- type: (dropdown) currently available item "SAML"
- displayName: (textInput)
- iconUrl: (textInput) / IconUpload: (uploadButton)
- contextHelp: (textInput)
- configuration: JSON object with multiple fields (see configuration for SAML)
- Once IdP is updated/created, prompt sp-metadata XML content to the user. User should be able to copy/download the content. Use `GET '/sso/saml/<idp-uuid>/sp-metadata'` api for getting sp-metadata.
- IdP can be removed using `DELETE /api/idp/<idp-uuid>`
### Backend
#### New model: "idp"
```jsx
{
id: uuid
displayName: string
iconUrl: string
contextHelp: string
configuration: JSON
}
```
#### Update: User model
- User should be uniquely identified by a "userId"/"id" string field. (ie. unique+nonNull constraints)
- Make email field optional. (i.e, only nonNull constraint)
- Make password field optional.
#### New fields in JWT token claims
```jsx
{
idpType: "saml" | "oidc" | null // (null for direct login),
idpId: "<uuid of idp>" // only present if idpType != null
samlNameId: "<saml nameid of the user>", // only present if idpType === "saml"
samlSloEnabled: "<true if SLO available for idP>", // only present if idpType === "saml"
}
```
#### Model field `idp.configuration` for SAML
```jsx
{
// metadata xml content provided from idP side.
// Render as fileUpload or textAreaInput
idpMetadataXml: "xml content",
// Render as checkbox. Only available if metadata xml has SingleLogoutService option present
enableSingleLogout: false,
// Render as dropdown list
// default value set to 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified'
// Other options:
// - urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
// - urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
// - urn:oasis:names:tc:SAML:2.0:nameid-format:entity
// - urn:oasis:names:tc:SAML:2.0:nameid-format:transient
// - urn:oasis:names:tc:SAML:2.0:nameid-format:persistent
// - urn:oasis:names:tc:SAML:2.0:nameid-format:encrypted
// - urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName
// - urn:oasis:names:tc:SAML:1.1:nameid-format:WindowsDomainQualifiedName
// - urn:oasis:names:tc:SAML:2.0:nameid-format:kerberos
nameIdFormat: 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified',
// Render as individual text inputs corresponding to each of given keys
// TextInputs are empty by default.
// All keys should be present in the API payload. default values sent to backend should be null.
// Empty values are mapped to null while sending to backend.
attributeMapping: {
// Below to this one, render a check box "Use SAML nameId as userId" - Upon checked, // userId textInput is disabled and value is cleared.
userId: "subject",
email: "email",
displayName: "nickName",
// Below to this one, render a check box "RBAC is not managed by idP" - Upon checked, // role textInput is disabled and value is cleared.
role: null,
},
// Render if role input is set in attributeMapping
// Render as individual text inputs corresponding to each of given keys. Use key as label
// Validate values to be valid regex
// Empty values not allowed. except for "USER"
// All keys should present in API payload.
roleMappingPattern: {
SUPERADMIN: "poweruser",
ADMIN: ".*manager",
USER: null, // no textInput for this one. render as label "unmatched"
},
// ---------- Advanced options section in UI -------------
// ========================================================================================================
// Render as checkbox
isLogoutRequestSigned: true,
// Render as checkbox
isAuthnRequestSigned: true,
// If isLogoutRequestSigned === true || isAuthnRequestSigned ===true render following 4 fields for user input. Otherwise all values should be null.
// Render as textArea or file chooser. Proper pem content expected
requestsSigningCertificate: "-----BEGIN CERTIFICATE ..... -----END CERTIFICATE-----",
// Render as textArea or file chooser. Proper pem content is expected
requestsSigningPrivateKey: "-----BEGIN RSA PRIVATE KEY----.... -----END RSA PRIVATE KEY-----",
// optional: Render as textInput. Can be left empty
requestsSigningPrivateKeyPassword: 'secret',
// Render as dropdown. rsa-sha256 is selected by default.
// Key and Values:
// rsa-sha256 => http://www.w3.org/2001/04/xmldsig-more#rsa-sha256
// rsa-sha1 => http://www.w3.org/2000/09/xmldsig#rsa-sha1
// rsa-sha512 => http://www.w3.org/2001/04/xmldsig-more#rsa-sha512
requestSignatureAlgorithm: 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256', // Auth0 only support sha256
// ========================================================================================================
// render as check box. If true, rest of the 6 fields should be rendered (similar as above fields), else they are set to null
isAssertionEncrypted: false,
assertionEncryptionCertificate: "-----BEGIN CERTIFICATE ..... -----END CERTIFICATE-----",,
assertionEncryptionPrivateKey: "-----BEGIN RSA PRIVATE KEY----.... -----END RSA PRIVATE KEY-----",,
assertionEncryptionPrivateKeyPassword: 'secret',
// Render as dropdown. AES_128 is selected by default
// Key and Values:
// AES_128: 'http://www.w3.org/2001/04/xmlenc#aes128-cbc',
// AES_256: 'http://www.w3.org/2001/04/xmlenc#aes256-cbc',
// TRI_DEC: 'http://www.w3.org/2001/04/xmlenc#tripledes-cbc',
// AES_128_GCM: 'http://www.w3.org/2009/xmlenc11#aes128-gcm'
assertionDataEncryptionAlgorithm: 'http://www.w3.org/2001/04/xmlenc#aes128-cbc',
// Render as dropdown. RSA_OAEP_MGF1P is selected by default.
// Key and Values:
// RSA_OAEP_MGF1P: 'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p',
// RSA_1_5: 'http://www.w3.org/2001/04/xmlenc#rsa-1_5',
assertionKeyEncryptionAlgorithm: 'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p',
// Render as dropdown:
// Values:
// - "sign-then-encrypt" (default)
// - "encrypt-then-sign".
assertionEncryptionOrder: "sign-then-encrypt",
}
```
#### APIs for managing Identity providers
```jsx
// IMPORTANT: This is an unauthenticated API. SO it should only provide necessary fields in the response!!)
GET '/api/auth/options' => {
idps: [{
type: "saml", uuid: "xxxx" , displayName: "AWS SSO", iconUrl: "https://example.com/aws-sso.png" , contextHelp: "Use your AWS SSO credentials for login. If you don't have one yet, please contact the IT team"
}, ],
isPasswordLoginDisabled: true
}
// NOTE: if we already have an application settings api, then reuse that.
PATCH '/api/settings' {
isPasswordLoginDisabled: true
}
POST '/api/idp' {
type: "saml" | "ldap" | "oidc" | "google" | "adfs" | "oam",
displayName: "My AWS Idp"
iconURL: "https://example.com/aws-sso.png",
configuration: { },
// server side fields: createdAt,updatedAt
} => {
id: "<uuid>",
type: "saml"
}
GET '/api/idp' => [
{ id: "xxxx", type, displayName, iconURL, configuration, createdAt,updatedAt},
{ id: "yyyyy", type, displayName, iconURL, configuration, createdAt,updatedAt }
...
]
DELETE '/api/idp/<idp-uuid>'
GET '/api/idp/<idp-uuid>' => returns whole `idp` model with given id
PUT '/api/idp/<idp-uuid>'
```
#### SAML specific APIs
Refer `sso.js` code for undestanding How dothese APIs work.
```jsx
GET '/sso/saml/<idp-uuid>/sp-metadata'
GET '/sso/saml/<idp-uuid>/sp-init-sso-redirect-binding'
GET '/sso/saml/<idp-uuid>/sp-init-sso-post-binding'
GET '/sso/saml/<idp-uuid>/sp-init-logout-redirect-binding'
POST '/sso/saml/<idp-uuid>/acs'
GET '/sso/saml/<idp-uuid>/slo'
POST '/sso/saml/<idp-uuid>/slo'
```
## Miscellaneous
--------------
idP service KB
- <https://developers.onelogin.com/saml/examples/authnrequest>
- <https://onelogin.service-now.com/support?id=kb_article&sys_id=912bb23edbde7810fe39dde7489619de&kb_category=93e869b0db185340d5505eea4b961934>
- <https://docs.aws.amazon.com/singlesignon/latest/userguide/attributemappingsconcept.html?icmpid=docs_sso_console#supportedssoattributes>
SAML Tools
- <https://samltool.io/>
- <https://www.samltool.com/online_tools.php>
- <https://www.samltool.com/sp_metadata.php>
Security
- <https://security.stackexchange.com/questions/42354/do-i-have-to-validate-saml2-inresponseto>
- <https://cheatsheetseries.owasp.org/cheatsheets/SAML_Security_Cheat_Sheet.html>
OSS
- <https://github.com/ritstudentgovernment/passport-saml-example/blob/master/app.js>
- <https://github.com/node-saml/passport-saml/issues/327#issuecomment-455454369>
- <https://dev.to/miteshkamat27/sso-express-js-passport-saml-3b6c>
- <https://github.com/authenio/react-samlify/blob/master/server.ts>
- <https://github.com/node-saml/passport-saml/issues/329>