---
# System prepended metadata

title: AssureQA idP integration

---

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