# 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>