├── .eslintrc.json ├── .gitattributes ├── .github ├── dependabot.yml ├── mergify.yml └── workflows │ ├── ci.yml │ └── publish.yml ├── .gitignore ├── .gitpod.yml ├── .husky ├── .gitignore ├── install.mjs ├── pre-commit └── pre-push ├── .prettierrc.js ├── Dockerfile ├── LICENSE ├── README.md ├── app.js ├── commitlint.config.js ├── eslint.config.mjs ├── index.js ├── lib ├── assertions.js ├── auth-code.js ├── crypto │ └── myinfo-signature.js └── express │ ├── index.js │ ├── myinfo │ ├── consent.js │ ├── controllers.js │ └── index.js │ ├── oidc │ ├── index.js │ ├── spcp.js │ ├── utils.js │ └── v2-ndi.js │ └── sgid.js ├── package-lock.json ├── package.json ├── public └── mockpass │ └── resources │ ├── css │ ├── animate.css │ ├── common.css │ ├── reset.css │ ├── style-baseline-small-media.css │ ├── style-baseline.css │ ├── style-common-small-media.css │ ├── style-common.css │ ├── style-homepage-small-media.css │ ├── style-homepage.css │ └── style-main.css │ ├── img │ ├── ajax-loader.gif │ ├── ask_cheryl_tab.png │ ├── background │ │ ├── large-device │ │ │ └── sp_bg.jpg │ │ ├── medium-device │ │ │ ├── ipad-bg.jpg │ │ │ └── ipad-landscape-sp-bg.jpg │ │ └── small-device │ │ │ └── mobile-sp-bg.jpg │ ├── carousel │ │ ├── large-device │ │ │ ├── how-to-setup-2fa-icon.png │ │ │ ├── register-icon.png │ │ │ ├── reset-password-icon.png │ │ │ ├── setup-2fa-icon.png │ │ │ └── update-acct-icon.png │ │ ├── medium-device │ │ │ ├── ipad-register-icon.png │ │ │ ├── ipad-reset-password-icon.png │ │ │ ├── ipad-setup-2fa-icon.png │ │ │ └── ipad-update-acct-icon.png │ │ └── small-device │ │ │ ├── mobile-register.png │ │ │ ├── mobile-reset-password-icon.png │ │ │ └── mobile-update-acct-icon.png │ ├── close.png │ ├── id-pw-icon.png │ ├── logo │ │ ├── mockpass-logo.png │ │ ├── mockpass-placeholder-logo.png │ │ └── mockpass_watermark.png │ ├── qr-icon.png │ ├── qr-shadow.png │ ├── refresh.jpg │ ├── sidebar-icons.png │ ├── sp-qr-unavailable.png │ └── utility-icon-black.png │ ├── js │ ├── bootstrap.min.js │ ├── jquery-3.5.1.js │ └── login-common.js │ └── plugins │ └── bootstrap-3.3.6 │ ├── css │ └── bootstrap.min.css │ └── fonts │ └── glyphicons-halflings-regular.woff2 └── static ├── certs ├── csr.pem ├── key.pem ├── key.pub ├── oidc-v2-asp-public.json ├── oidc-v2-asp-secret.json ├── oidc-v2-rp-public.json ├── oidc-v2-rp-secret.json ├── server.crt ├── spcp-csr.pem ├── spcp-key.pem └── spcp.crt ├── html ├── consent.html └── login-page.html └── myinfo └── v3.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:prettier/recommended" 5 | ], 6 | "parserOptions": { 7 | "ecmaVersion": 2020 8 | }, 9 | "env": { 10 | "node": true, 11 | "es6": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set to not apply eol conversion for subresource integrity check 2 | public/mockpass/resources/js/*.js -text -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | commit-message: 13 | prefix: "chore(deps):" 14 | target-branch: "dependabot" 15 | -------------------------------------------------------------------------------- /.github/mergify.yml: -------------------------------------------------------------------------------- 1 | pull_request_rules: 2 | - name: command dependabot to squash and merge 3 | conditions: 4 | - 'author=dependabot[bot]' 5 | - 'check-success~=CI' 6 | - 'title~=bump [^\s]+ from ([\d]+)\..+ to \1\.' 7 | - 'base=dependabot' 8 | actions: 9 | review: 10 | type: APPROVE 11 | merge: 12 | method: squash 13 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: 3 | push: 4 | pull_request: 5 | types: [opened, reopened] 6 | jobs: 7 | ci: 8 | name: CI 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Use Node.js 13 | uses: actions/setup-node@v3 14 | with: 15 | node-version: 'lts/*' 16 | cache: 'npm' 17 | cache-dependency-path: '**/package-lock.json' 18 | - run: npm ci 19 | - run: npx lockfile-lint --type npm --path package-lock.json --validate-https --allowed-hosts npm 20 | - run: npm run lint 21 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | publish-npm: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: actions/setup-node@v3 13 | with: 14 | node-version: 'lts/*' 15 | cache: 'npm' 16 | cache-dependency-path: '**/package-lock.json' 17 | registry-url: https://registry.npmjs.org/ 18 | - run: npm ci 19 | - run: npm publish --access public 20 | env: 21 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 22 | publish-docker: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v4 26 | - uses: actions/setup-node@v3 27 | with: 28 | node-version: 'lts/*' 29 | cache: 'npm' 30 | cache-dependency-path: '**/package-lock.json' 31 | registry-url: https://registry.npmjs.org/ 32 | - name: Set up QEMU 33 | uses: docker/setup-qemu-action@v3 34 | - name: Set up Docker Buildx 35 | uses: docker/setup-buildx-action@v3 36 | - name: Login to Docker Hub 37 | uses: docker/login-action@v3 38 | with: 39 | username: ${{ secrets.DOCKER_USER }} 40 | password: ${{ secrets.DOCKER_PASS }} 41 | - run: echo TAGNAME=`echo ${{ github.ref_name }} | sed 's/v//'` >> ${GITHUB_ENV} 42 | - name: Build and push 43 | uses: docker/build-push-action@v6 44 | with: 45 | push: true 46 | platforms: | 47 | linux/arm64/v8 48 | linux/amd64 49 | tags: | 50 | opengovsg/mockpass:latest 51 | opengovsg/mockpass:${{ env.TAGNAME }} 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | # VS Code settings 64 | .vscode/settings.json 65 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | tasks: 2 | - init: npm install 3 | command: SHOW_LOGIN_PAGE=true npm start 4 | ports: 5 | - port: 5156 -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/install.mjs: -------------------------------------------------------------------------------- 1 | // Skip Husky install in production and CI 2 | if (process.env.NODE_ENV === 'production' || process.env.CI === 'true') { 3 | process.exit(0) 4 | } 5 | const husky = (await import('husky')).default 6 | console.log(husky()) 7 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx lint-staged 2 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | npx commitlint --from origin/main --to HEAD --verbose 2 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: false, 3 | trailingComma: 'all', 4 | singleQuote: true, 5 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:slim 2 | 3 | WORKDIR /usr/src/mockpass 4 | 5 | COPY package* ./ 6 | 7 | COPY ./.husky ./.husky 8 | 9 | RUN npm ci 10 | 11 | COPY . ./ 12 | 13 | CMD ["node", "index.js"] 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Open Government Products, Government Technology Agency of Singapore 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MockPass 2 | 3 | A mock Singpass/Corppass/Myinfo v3/sgID v2 server for dev purposes 4 | 5 | ## Quick Start (hosted remotely on Gitpod) 6 | 7 | [![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/opengovsg/mockpass) 8 | 9 | - Click the ready-to-code badge above 10 | - Wait for MockPass to start 11 | - Make port 5156 public 12 | - Open browser to note the URL hosting MockPass 13 | - Configure your application per local machine quick start, changing 14 | the `localhost:5156` to the Gitpod hostname 15 | 16 | ## Quick Start (hosted locally) 17 | 18 | ### Singpass v2 (NDI OIDC) 19 | 20 | Configure your application to point to the following endpoints: 21 | 22 | - http://localhost:5156/singpass/v2/.well-known/openid-configuration 23 | - http://localhost:5156/singpass/v2/.well-known/keys 24 | - http://localhost:5156/singpass/v2/authorize 25 | - http://localhost:5156/singpass/v2/token 26 | 27 | Configure your application (or MockPass) with keys: 28 | 29 | - EITHER configure MockPass with your application's JWKS endpoint URL using the env 30 | var `SP_RP_JWKS_ENDPOINT`. 31 | - OR configure your application to use the private keys from 32 | `static/certs/oidc-v2-rp-secret.json`. 33 | 34 | MockPass accepts any value for `client_id` and `redirect_uri`. 35 | 36 | | Configuration item | Explanation | 37 | | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 38 | | Client signing and encryption keys | **Overview:** When client makes any request, what signing key is used to verify the client's signature on the client assertion, and what encryption key is used to encrypt the data payload.
**Default:** static keyset `static/certs/oidc-v2-rp-public.json` is used.
**How to configure:** Set the env var `SP_RP_JWKS_ENDPOINT` to a JWKS URL that MockPass can connect to. This can be a HTTP or HTTPS URL. | 39 | | Login page | **Overview:** When client makes an authorize request, whether MockPass sends the client to a login page, instead of completing the login silently.
**Default:** Disabled for all requests.
**How to configure:** Enable for all requests by default by setting the env var `SHOW_LOGIN_PAGE` to `true`. Regardless of the default, you can override on a per-request basis by sending the HTTP request header `X-Show-Login-Page` with the value `true`.
**Detailed effect:** When login page is disabled, MockPass will immediately complete login and redirect to the `redirect_uri`. The profile used will be (in order of decreasing precedence) the profile specified in HTTP request headers (`X-Custom-NRIC` and `X-Custom-UUID` must both be specified), the profile with the NRIC specified in the env var `MOCKPASS_NRIC`, or the first profile in MockPass' static data.
When login page is enabled, MockPass returns a HTML page with a form that is used to complete the login. The client may select an existing profile, or provide a custom NRIC and UUID on the form. | 40 | | ID token exchange | **Overview:** Singpass uses the client's [profile](https://docs.developer.singpass.gov.sg/docs/technical-specifications/singpass-authentication-api/2.-token-endpoint/authorization-code-grant#id-token-structure) to decide the format of the id token to send across.
**Default:** `direct`
**How to configure:** To set this, set the env var (`SINGPASS_CLIENT_PROFILE`) to the desired value | 41 | 42 | ### Corppass v2 (Corppass OIDC) 43 | 44 | Configure your application to point to the following endpoints: 45 | 46 | - http://localhost:5156/corppass/v2/.well-known/openid-configuration 47 | - http://localhost:5156/corppass/v2/.well-known/keys 48 | - http://localhost:5156/corppass/v2/authorize 49 | - http://localhost:5156/corppass/v2/token 50 | 51 | Configure your application (or MockPass) with keys: 52 | 53 | - EITHER configure MockPass with your application's JWKS endpoint URL using the env 54 | var `CP_RP_JWKS_ENDPOINT`. HTTP/HTTPS endpoints are supported. 55 | - OR configure your application to use the private keys from 56 | `static/certs/oidc-v2-rp-secret.json`. 57 | 58 | MockPass accepts any value for `client_id` and `redirect_uri`. 59 | 60 | | Configuration item | Explanation | 61 | | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 62 | | Client signing and encryption keys | **Overview:** When client makes any request, what signing key is used to verify the client's signature on the client assertion, and what encryption key is used to encrypt the data payload.
**Default:** static keyset `static/certs/oidc-v2-rp-public.json` is used.
**How to configure:** Set the env var `CP_RP_JWKS_ENDPOINT` to a JWKS URL that MockPass can connect to. This can be a HTTP or HTTPS URL. | 63 | | Login page | **Overview:** When client makes an authorize request, whether MockPass sends the client to a login page, instead of completing the login silently.
**Default:** Disabled for all requests.
**How to configure:** Enable for all requests by default by setting the env var `SHOW_LOGIN_PAGE` to `true`. Regardless of the default, you can override on a per-request basis by sending the HTTP request header `X-Show-Login-Page` with the value `true`.
**Detailed effect:** When login page is disabled, MockPass will immediately complete login and redirect to the `redirect_uri`. The profile used will be (in order of decreasing precedence) the profile specified in HTTP request headers (`X-Custom-NRIC`, `X-Custom-UUID`, `X-Custom-UEN` must all be specified), the profile with the NRIC specified in the env var `MOCKPASS_NRIC`, or the first profile in MockPass' static data.
When login page is enabled, MockPass returns a HTML page with a form that is used to complete the login. The client may select an existing profile, or provide a custom NRIC, UUID and UEN on the form. | 64 | 65 | ### Myinfo v3 66 | 67 | Configure your application to point to the following endpoints: 68 | 69 | - http://localhost:5156/myinfo/v3/authorise 70 | - http://localhost:5156/myinfo/v3/token 71 | - http://localhost:5156/myinfo/v3/person-basic (exclusive to government systems) 72 | - http://localhost:5156/myinfo/v3/person 73 | 74 | Configure your application (or MockPass) with certificates/keys: 75 | 76 | - Provide your application with 77 | the certificate `static/certs/spcp.crt` as the Myinfo public certificate. 78 | - EITHER configure MockPass with your application's X.509 certificate by setting 79 | the env vars `SERVICE_PROVIDER_PUB_KEY` and `SERVICE_PROVIDER_CERT_PATH` to 80 | the path to the certificate in PEM format. Self-signed or untrusted 81 | certificates are supported. 82 | - OR configure your application to use the certificate and private key 83 | from `static/certs/(server.crt|key.pem)`. 84 | 85 | MockPass accepts any value for `client_id`, `redirect_uri` and `sp_esvcId`. 86 | The `client_secret` value will be checked if configured, see below. 87 | 88 | Only the profiles (NRICs) that have entries in Mockpass' personas dataset will 89 | succeed, using other NRICs will result in an error. See the list of personas in 90 | [static/myinfo/v3.json](static/myinfo/v3.json). 91 | 92 | | Configuration item | Explanation | 93 | | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 94 | | Client certificate | **Overview:** When client makes any request, what certificate is used to verify the request signature, and what certificate is used to encrypt the data payload.
**Default:** static certificate/key `static/certs/(server.crt\|key.pub)` are used.
**How to configure:** Set the env var `SERVICE_PROVIDER_PUB_KEY` to the path to a public key PEM file, and `SERVICE_PROVIDER_CERT_PATH` to the path to a certificate PEM file. (A certificate PEM file can also be provided to `SERVICE_PROVIDER_PUB_KEY`, despite the env var name.) | 95 | | Client secret | **Overview:** When client makes a Token request, whether MockPass verifies the request signature.
**Default:** Disabled.
**How to configure:** Enable for all requests by setting the env var `SERVICE_PROVIDER_MYINFO_SECRET` to some non-blank string. Provide this value to your application as well. | 96 | | Payload encryption | **Overview:** When client makes a Person or Person-Basic request, whether MockPass encrypts the data payload. When client makes a Person request, whether MockPass verifies the request signature.
**Default:** Disabled.
**How to configure:** Enable for all requests by setting the env var `ENCRYPT_MYINFO` to `true`. | 97 | 98 | To emulate the equivalent of the Test environment on Myinfo v3, you must both 99 | set a client secret and enable payload encryption on MockPass. 100 | 101 | ### sgID v2 102 | 103 | Configure your application to point to the following endpoints: 104 | 105 | - http://localhost:5156/v2/.well-known/openid-configuration 106 | - http://localhost:5156/v2/.well-known/jwks.json 107 | - http://localhost:5156/v2/oauth/authorize 108 | - http://localhost:5156/v2/oauth/token 109 | - http://localhost:5156/v2/oauth/userinfo 110 | 111 | Configure your application (or MockPass) with certificates/keys: 112 | 113 | - Provide your application with the certificate `static/certs/spcp.crt` as the 114 | sgID public key, or use the signing key published at the JWKS endpoint. 115 | - EITHER configure MockPass with your application's X.509 certificate using the 116 | env var `SERVICE_PROVIDER_PUB_KEY`, as the path to the certificate in PEM 117 | format. Self-signed or untrusted certificates are supported. 118 | - OR configure your application to use the certificate and private key 119 | from `static/certs/(server.crt|key.pem)`. 120 | 121 | MockPass accepts any value for `client_id`, `client_secret` and `redirect_uri`. 122 | 123 | Only the profiles (NRICs) that have entries in Mockpass' personas dataset will 124 | succeed, using other NRICs will result in an error. See the list of personas in 125 | [static/myinfo/v3.json](static/myinfo/v3.json). 126 | 127 | If the Public Officer Employment Details data item is requested, the 128 | `pocdex.public_officer_details` scope data is sourced from the 129 | `publicofficerdetails` data key (where present) on personas. 130 | Most personas do not have this data key configured, and will result in a `"NA"` 131 | response instead of an stringified array. As these personas are not identified 132 | in the login page dropdown, please check the personas dataset linked above to 133 | identify them. 134 | The `pocdex.number_of_employments` scope is not supported. 135 | 136 | | Configuration item | Explanation | 137 | | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 138 | | Client certificate | **Overview:** When client makes any request, what certificate is used to verify the request signature, and what certificate is used to encrypt the data payload.
**Default:** static key `static/certs/key.pub` is used.
**How to configure:** Set the env var `SERVICE_PROVIDER_PUB_KEY` to the path to a public key PEM file. (A certificate PEM file can also be provided, despite the env var name.) | 139 | | Login page | **Overview:** When client makes an authorize request, whether MockPass sends the client to a login page, instead of completing the login silently.
**Default:** Disabled for all requests.
**How to configure:** Enable for all requests by default by setting the env var `SHOW_LOGIN_PAGE` to `true`. Regardless of the default, you can override on a per-request basis by sending the HTTP request header `X-Show-Login-Page` with the value `true`.
**Detailed effect:** When login page is disabled, MockPass will immediately complete login and redirect to the `redirect_uri`. The profile used will be (in order of decreasing precedence) the profile with the NRIC specified in the env var `MOCKPASS_NRIC`, or the first profile in MockPass' static data.
When login page is enabled, MockPass returns a HTML page with a form that is used to complete the login. The client may select an existing profile, or provide a custom NRIC and UUID on the form. | 140 | 141 | ### Singpass/Corppass v1 (legacy) 142 | 143 | **The v1 APIs should no longer be in use, see the v2 APIs above!** 144 | 145 | Configure your application to point to the following endpoints: 146 | 147 | Singpass (v1 - Singpass OIDC): 148 | 149 | - http://localhost:5156/singpass/authorize - OIDC login redirect with optional page 150 | - http://localhost:5156/singpass/token - receives OIDC authorization code and returns id_token 151 | 152 | Corppass (v1 - Corppass OIDC): 153 | 154 | - http://localhost:5156/corppass/authorize - OIDC login redirect with optional page 155 | - http://localhost:5156/corppass/token - receives OIDC authorization code and returns id_token 156 | 157 | Configure your application with keys and/or certificates: 158 | 159 | Provide your application with the certificate `static/certs/spcp.crt` as the 160 | Singpass/Corppass public certificate. 161 | Provide the path to your application's X.509 certificate in PEM 162 | format as env var `SERVICE_PROVIDER_CERT_PATH` when running MockPass. 163 | Self-signed or untrusted certificates are supported. 164 | Alternatively, provide your application with the certificate and private key 165 | from `static/certs/(server.crt|key.pem)`. 166 | 167 | ### Run MockPass 168 | 169 | Common configuration: 170 | 171 | | Configuration item | Explanation | 172 | | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 173 | | Port number | **Overview:** What port number MockPass will listen for HTTP requests on.
**Default:** 5156.
**How to configure:** Set the env var `MOCKPASS_PORT` or `PORT` to some port number. | 174 | | Stateless Mode | **Overview:** Enable for environments where the state of the process is not guaranteed, such as in serverless contexts.
**Default:** not set.
**How to configure:** Set the env var `MOCKPASS_STATELESS` to `true` or `false`. | 175 | 176 | Run MockPass: 177 | 178 | ``` 179 | $ npm install @opengovsg/mockpass 180 | 181 | # Configure the listening port if desired, defaults to 5156 182 | $ export MOCKPASS_PORT=5156 183 | 184 | # Configure any other options if required 185 | $ export SHOW_LOGIN_PAGE=false 186 | $ export MOCKPASS_NRIC=S8979373D 187 | $ export SERVICE_PROVIDER_MYINFO_SECRET= 188 | $ export ENCRYPT_MYINFO=false 189 | 190 | $ npx mockpass 191 | MockPass listening on 5156 192 | 193 | # Alternatively, just run directly with npx 194 | MOCKPASS_PORT=5156 SHOW_LOGIN_PAGE=false MOCKPASS_NRIC=S8979373D npx @opengovsg/mockpass@latest 195 | ``` 196 | 197 | ## Contributing 198 | 199 | We welcome contributions to code open-sourced by the Government Technology 200 | Agency of Singapore. All contributors will be asked to sign a Contributor 201 | License Agreement (CLA) in order to ensure that everybody is free to use their 202 | contributions. 203 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const fs = require('fs') 3 | const express = require('express') 4 | const morgan = require('morgan') 5 | const path = require('path') 6 | require('dotenv').config() 7 | 8 | const { 9 | configOIDC, 10 | configOIDCv2, 11 | configMyInfo, 12 | configSGID, 13 | } = require('./lib/express') 14 | 15 | const serviceProvider = { 16 | cert: fs.readFileSync( 17 | path.resolve( 18 | __dirname, 19 | process.env.SERVICE_PROVIDER_CERT_PATH || './static/certs/server.crt', 20 | ), 21 | ), 22 | pubKey: fs.readFileSync( 23 | path.resolve( 24 | __dirname, 25 | process.env.SERVICE_PROVIDER_PUB_KEY || './static/certs/key.pub', 26 | ), 27 | ), 28 | } 29 | 30 | const cryptoConfig = { 31 | signAssertion: process.env.SIGN_ASSERTION !== 'false', // default to true to be backward compatable 32 | signResponse: process.env.SIGN_RESPONSE !== 'false', 33 | encryptAssertion: process.env.ENCRYPT_ASSERTION !== 'false', 34 | resolveArtifactRequestSigned: 35 | process.env.RESOLVE_ARTIFACT_REQUEST_SIGNED !== 'false', 36 | } 37 | 38 | const isStateless = process.env.MOCKPASS_STATELESS === 'true' 39 | 40 | const options = { 41 | serviceProvider, 42 | showLoginPage: (req) => 43 | (req.header('X-Show-Login-Page') || process.env.SHOW_LOGIN_PAGE) === 'true', 44 | encryptMyInfo: process.env.ENCRYPT_MYINFO === 'true', 45 | cryptoConfig, 46 | isStateless, 47 | } 48 | 49 | const app = express() 50 | app.use(morgan('combined')) 51 | 52 | configOIDC(app, options) 53 | configOIDCv2(app, options) 54 | configSGID(app, options) 55 | 56 | configMyInfo.consent(app, options) 57 | configMyInfo.v3(app, options) 58 | 59 | app.enable('trust proxy') 60 | app.use(express.static(path.join(__dirname, 'public'))) 61 | 62 | exports.app = app 63 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | rules: { 4 | 'subject-case': [2, 'never', ['start-case', 'pascal-case', 'upper-case']], 5 | 'scope-case': [2, 'always', ['pascal-case', 'lower-case', 'camel-case']], 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from "globals"; 2 | import path from "node:path"; 3 | import { fileURLToPath } from "node:url"; 4 | import js from "@eslint/js"; 5 | import { FlatCompat } from "@eslint/eslintrc"; 6 | 7 | const __filename = fileURLToPath(import.meta.url); 8 | const __dirname = path.dirname(__filename); 9 | const compat = new FlatCompat({ 10 | baseDirectory: __dirname, 11 | recommendedConfig: js.configs.recommended, 12 | allConfig: js.configs.all 13 | }); 14 | 15 | export default [...compat.extends("eslint:recommended", "plugin:prettier/recommended"), { 16 | languageOptions: { 17 | globals: { 18 | ...globals.node, 19 | }, 20 | 21 | ecmaVersion: 2020, 22 | sourceType: "commonjs", 23 | }, 24 | }]; -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const { app } = require('./app') 3 | 4 | const PORT = process.env.MOCKPASS_PORT || process.env.PORT || 5156 5 | 6 | app.listen(PORT, (err) => 7 | err 8 | ? console.error('Unable to start MockPass', err) 9 | : console.warn(`MockPass listening on ${PORT}`), 10 | ) 11 | -------------------------------------------------------------------------------- /lib/assertions.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto') 2 | const fs = require('fs') 3 | const jose = require('node-jose') 4 | const path = require('path') 5 | 6 | const readFrom = (p) => fs.readFileSync(path.resolve(__dirname, p), 'utf8') 7 | 8 | const signingPem = fs.readFileSync( 9 | path.resolve(__dirname, '../static/certs/spcp-key.pem'), 10 | ) 11 | 12 | const hashToken = (token) => { 13 | const fullHash = crypto.createHash('sha256') 14 | fullHash.update(token, 'utf8') 15 | const fullDigest = fullHash.digest() 16 | const digestBuffer = fullDigest.slice(0, fullDigest.length / 2) 17 | if (Buffer.isEncoding('base64url')) { 18 | return digestBuffer.toString('base64url') 19 | } else { 20 | const fromBase64 = (base64String) => 21 | base64String.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_') 22 | return fromBase64(digestBuffer.toString('base64')) 23 | } 24 | } 25 | 26 | const myinfo = { 27 | v3: JSON.parse(readFrom('../static/myinfo/v3.json')), 28 | } 29 | 30 | const oidc = { 31 | singPass: [ 32 | { nric: 'S8979373D', uuid: 'a9865837-7bd7-46ac-bef4-42a76a946424' }, 33 | { nric: 'S8116474F', uuid: 'f4b70aea-d639-4b79-b8d9-8ace5875f6b1' }, 34 | { nric: 'S8723211E', uuid: '178478de-fed7-4c03-a75e-e68c44d0d5f0' }, 35 | { nric: 'S5062854Z', uuid: '1bd2e743-8681-4079-a557-6a66a8d16386' }, 36 | { nric: 'T0066846F', uuid: '14f7ee8f-9e64-4170-a529-e55ca7578e2b' }, 37 | { nric: 'F9477325W', uuid: '2135fe5c-d07b-49d3-b960-aabb0ff2e05a' }, 38 | { nric: 'S3000024B', uuid: 'b5630beb-e3ee-4a31-aec5-534cdc087fd8' }, 39 | { nric: 'S6005040F', uuid: '6c6745d9-e6c5-40ee-8c96-5d737ddbc5e4' }, 40 | { nric: 'S6005041D', uuid: 'bd3fd1e0-c807-4b07-bbe4-b567cab54b8c' }, 41 | { nric: 'S6005042B', uuid: '2dd788c0-d11f-4d5b-99af-b89d2389b474' }, 42 | { nric: 'S6005043J', uuid: 'eb196477-36b3-4c0f-ae5e-2172e2f6a6d8' }, 43 | { nric: 'S6005044I', uuid: '843ebc6b-1de1-4d46-b1dd-9ad4aeac3a27' }, 44 | { nric: 'S6005045G', uuid: 'caafaedc-f369-498a-9e35-27e9cb7f0de2' }, 45 | { nric: 'S6005046E', uuid: 'f9b37d06-de3f-4c4f-8331-37a3b2ee6cb4' }, 46 | { nric: 'S6005047C', uuid: '57620e0f-fdf9-4f3e-a8f6-f6088e151395' }, 47 | { nric: 'S6005064C', uuid: '80952b2f-3455-4b59-b50f-39afbc418271' }, 48 | { nric: 'S6005065A', uuid: '3af48e26-69a1-43e3-b5f2-303098ef3210' }, 49 | { nric: 'S6005066Z', uuid: '8b2f8213-2fe9-493a-ac95-0b55e319e689' }, 50 | { nric: 'S6005037F', uuid: 'ae3d1d8c-6d14-449e-8ed1-9ce3d5e67607' }, 51 | { nric: 'S6005038D', uuid: '23d3bb45-a324-46d6-b0d9-2e94194ed9ae' }, 52 | { nric: 'S6005039B', uuid: '9ac807a2-5217-417a-a8d1-d7018b002b3f' }, 53 | { nric: 'G1612357P', uuid: 'eb125a02-3137-486f-9262-eab3e0c57a5f' }, 54 | { nric: 'G1612358M', uuid: 'd821900c-663d-4552-a753-a2e1cf8d124f' }, 55 | { nric: 'F1612359P', uuid: '08df8d35-600c-45fd-a812-b37a27b7856a' }, 56 | { nric: 'F1612360U', uuid: '1e90b698-23af-4acb-9fb4-eb5a80f444b6' }, 57 | { nric: 'F1612361R', uuid: 'bc134ee1-f104-4b26-9839-32047fecb963' }, 58 | { nric: 'F1612362P', uuid: '285e8366-f3bd-48b4-8153-b47260fc9f56' }, 59 | { nric: 'F1612363M', uuid: '379bc106-d3db-492c-a38e-fd27642ef47f' }, 60 | { nric: 'F1612364K', uuid: '108fa3ff-c85c-461e-ba1f-8edef62b68e2' }, 61 | { nric: 'F1612365W', uuid: '1275ae4e-02d2-4b09-9573-36ac610ede89' }, 62 | { nric: 'F1612366T', uuid: '23c6a3a4-d9d8-445f-a588-9d91831980a6' }, 63 | { nric: 'F1612367Q', uuid: '0c400961-eb00-425a-8df4-6656b0b9245a' }, 64 | { nric: 'F1612358R', uuid: '45669f5c-e9ac-43c6-bcd2-9c3757f1fa1c' }, 65 | { nric: 'F1612354N', uuid: 'c38ddb2d-9e5d-45c2-bb70-8ccb54fc8320' }, 66 | { nric: 'F1612357U', uuid: 'f904a2b1-4b61-47e2-bdad-e2d606325e20' }, 67 | { nric: 'Y4581892I', uuid: 'acf8edda-bfdf-45fc-b140-a6ec6955d857' }, 68 | { nric: 'Y7654321K', uuid: '9916f054-488e-4894-8299-412e46d89e67' }, 69 | { nric: 'Y1234567P', uuid: '0fdcc18f-840b-4b35-80ee-44094a6cc66f' }, 70 | ...Object.keys(myinfo.v3.personas).map((nric) => ({ 71 | nric, 72 | uuid: myinfo.v3.personas[nric].uuid.value, 73 | })), 74 | ], 75 | corpPass: [ 76 | { 77 | nric: 'S8979373D', 78 | uuid: 'a9865837-7bd7-46ac-bef4-42a76a946424', 79 | name: 'Name of S8979373D', 80 | isSingPassHolder: true, 81 | uen: '123456789A', 82 | }, 83 | { 84 | nric: 'S8116474F', 85 | uuid: 'f4b70aea-d639-4b79-b8d9-8ace5875f6b1', 86 | name: 'Name of S8116474F', 87 | isSingPassHolder: true, 88 | uen: '123456789A', 89 | }, 90 | { 91 | nric: 'S8723211E', 92 | uuid: '178478de-fed7-4c03-a75e-e68c44d0d5f0', 93 | name: 'Name of S8723211E', 94 | isSingPassHolder: true, 95 | uen: '123456789A', 96 | }, 97 | { 98 | nric: 'S5062854Z', 99 | uuid: '1bd2e743-8681-4079-a557-6a66a8d16386', 100 | name: 'Name of S5062854Z', 101 | isSingPassHolder: true, 102 | uen: '123456789B', 103 | }, 104 | { 105 | nric: 'T0066846F', 106 | uuid: '14f7ee8f-9e64-4170-a529-e55ca7578e2b', 107 | name: 'Name of T0066846F', 108 | isSingPassHolder: true, 109 | uen: '123456789B', 110 | }, 111 | { 112 | nric: 'F9477325W', 113 | uuid: '2135fe5c-d07b-49d3-b960-aabb0ff2e05a', 114 | name: 'Name of F9477325W', 115 | isSingPassHolder: false, 116 | uen: '123456789B', 117 | }, 118 | { 119 | nric: 'S3000024B', 120 | uuid: 'b5630beb-e3ee-4a31-aec5-534cdc087fd8', 121 | name: 'Name of S3000024B', 122 | isSingPassHolder: true, 123 | uen: '123456789C', 124 | }, 125 | { 126 | nric: 'S6005040F', 127 | uuid: '6c6745d9-e6c5-40ee-8c96-5d737ddbc5e4', 128 | name: 'Name of S6005040F', 129 | isSingPassHolder: true, 130 | uen: '123456789C', 131 | }, 132 | ], 133 | create: { 134 | singPass: ( 135 | { nric, uuid }, 136 | iss, 137 | aud, 138 | nonce, 139 | accessToken = crypto.randomBytes(15).toString('hex'), 140 | ) => { 141 | let sub 142 | const sfa = { 143 | Y4581892I: { fid: 'G730Z-H5P96', coi: 'DE', RP: 'CORPPASS' }, 144 | Y7654321K: { fid: '123456789', coi: 'CN', RP: 'IRAS' }, 145 | Y1234567P: { fid: 'G730Z-H5P96', coi: 'MY', RP: 'CORPPASS' }, 146 | } 147 | if (nric.startsWith('Y')) { 148 | const sfaAccount = sfa[nric] 149 | ? sfa[nric] 150 | : { fid: 'G730Z-H5P96', coi: 'DE', RP: 'CORPPASS' } 151 | sub = `s=${nric},fid=${sfaAccount.fid},coi=${sfaAccount.coi},u=${uuid}` 152 | } else { 153 | sub = `s=${nric},u=${uuid}` 154 | } 155 | const accessTokenHash = hashToken(accessToken) 156 | 157 | const refreshToken = crypto.randomBytes(20).toString('hex') 158 | const refreshTokenHash = hashToken(refreshToken) 159 | 160 | return { 161 | accessToken, 162 | refreshToken, 163 | idTokenClaims: { 164 | rt_hash: refreshTokenHash, 165 | at_hash: accessTokenHash, 166 | iat: Math.floor(Date.now() / 1000), 167 | exp: Math.floor(Date.now() / 1000) + 24 * 60 * 60, 168 | iss, 169 | amr: ['pwd'], 170 | aud, 171 | sub, 172 | ...(nonce ? { nonce } : {}), 173 | }, 174 | } 175 | }, 176 | corpPass: async ( 177 | { nric, uuid, name, isSingPassHolder, uen }, 178 | iss, 179 | aud, 180 | nonce, 181 | ) => { 182 | const baseClaims = { 183 | iat: Math.floor(Date.now() / 1000), 184 | exp: Math.floor(Date.now() / 1000) + 24 * 60 * 60, 185 | iss, 186 | aud, 187 | } 188 | 189 | const sub = `s=${nric},u=${uuid},c=SG` 190 | 191 | const accessTokenClaims = { 192 | ...baseClaims, 193 | authorization: { 194 | EntityInfo: {}, 195 | AccessInfo: {}, 196 | TPAccessInfo: {}, 197 | }, 198 | } 199 | 200 | const signingKey = await jose.JWK.asKey(signingPem, 'pem') 201 | const accessToken = await jose.JWS.createSign( 202 | { format: 'compact' }, 203 | signingKey, 204 | ) 205 | .update(JSON.stringify(accessTokenClaims)) 206 | .final() 207 | 208 | const accessTokenHash = hashToken(accessToken) 209 | 210 | const refreshToken = crypto.randomBytes(20).toString('hex') 211 | const refreshTokenHash = hashToken(refreshToken) 212 | 213 | return { 214 | accessToken, 215 | refreshToken, 216 | idTokenClaims: { 217 | ...baseClaims, 218 | rt_hash: refreshTokenHash, 219 | at_hash: accessTokenHash, 220 | amr: ['pwd'], 221 | sub, 222 | ...(nonce ? { nonce } : {}), 223 | userInfo: { 224 | CPAccType: 'User', 225 | CPUID_FullName: name, 226 | ISSPHOLDER: isSingPassHolder ? 'YES' : 'NO', 227 | }, 228 | entityInfo: { 229 | CPEntID: uen, 230 | CPEnt_TYPE: 'UEN', 231 | CPEnt_Status: 'Registered', 232 | CPNonUEN_Country: '', 233 | CPNonUEN_RegNo: '', 234 | CPNonUEN_Name: '', 235 | }, 236 | }, 237 | } 238 | }, 239 | }, 240 | } 241 | 242 | module.exports = { 243 | oidc, 244 | myinfo, 245 | } 246 | -------------------------------------------------------------------------------- /lib/auth-code.js: -------------------------------------------------------------------------------- 1 | const ExpiryMap = require('expiry-map') 2 | const crypto = require('crypto') 3 | 4 | const AUTH_CODE_TIMEOUT = 5 * 60 * 1000 5 | const profileAndNonceStore = new ExpiryMap(AUTH_CODE_TIMEOUT) 6 | 7 | const generateAuthCode = ( 8 | { profile, scopes, nonce }, 9 | { isStateless = false }, 10 | ) => { 11 | const authCode = isStateless 12 | ? Buffer.from(JSON.stringify({ profile, scopes, nonce })).toString( 13 | 'base64url', 14 | ) 15 | : crypto.randomBytes(45).toString('base64') 16 | 17 | profileAndNonceStore.set(authCode, { profile, scopes, nonce }) 18 | return authCode 19 | } 20 | 21 | const lookUpByAuthCode = (authCode, { isStateless = false }) => { 22 | return isStateless 23 | ? JSON.parse(Buffer.from(authCode, 'base64url').toString('utf-8')) 24 | : profileAndNonceStore.get(authCode) 25 | } 26 | 27 | module.exports = { generateAuthCode, lookUpByAuthCode } 28 | -------------------------------------------------------------------------------- /lib/crypto/myinfo-signature.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const qs = require('node:querystring') 3 | 4 | const pki = function pki(authHeader, req, context = {}) { 5 | const authHeaderFieldPairs = _(authHeader) 6 | .replace(/"/g, '') 7 | .split(',') 8 | .map((v) => v.replace('=', '~').split('~')) 9 | 10 | const authHeaderFields = Object.fromEntries(authHeaderFieldPairs) 11 | 12 | const url = `${req.protocol}://${req.get('host')}${req.baseUrl}${req.path}` 13 | 14 | const { method: httpMethod, query, body } = req 15 | 16 | const { signature, app_id, nonce, timestamp } = authHeaderFields 17 | 18 | const params = Object.assign( 19 | {}, 20 | query, 21 | body, 22 | { 23 | nonce, 24 | app_id, 25 | signature_method: 'RS256', 26 | timestamp, 27 | }, 28 | context.client_secret && context.redirect_uri ? context : {}, 29 | ) 30 | 31 | const sortedParams = Object.fromEntries( 32 | Object.entries(params).sort(([k1], [k2]) => k1.localeCompare(k2)), 33 | ) 34 | 35 | const baseString = 36 | httpMethod.toUpperCase() + 37 | '&' + 38 | url + 39 | '&' + 40 | qs.unescape(qs.stringify(sortedParams)) 41 | 42 | return { signature, baseString } 43 | } 44 | 45 | module.exports = { pki } 46 | -------------------------------------------------------------------------------- /lib/express/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ...require('./oidc'), 3 | configMyInfo: require('./myinfo'), 4 | configSGID: require('./sgid'), 5 | } 6 | -------------------------------------------------------------------------------- /lib/express/myinfo/consent.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const cookieParser = require('cookie-parser') 3 | const fs = require('fs') 4 | const { pick } = require('lodash') 5 | const { render } = require('mustache') 6 | const path = require('path') 7 | const qs = require('querystring') 8 | const { v1: uuid } = require('uuid') 9 | 10 | const assertions = require('../../assertions') 11 | const { lookUpByAuthCode } = require('../../auth-code') 12 | 13 | const MYINFO_ASSERT_ENDPOINT = '/consent/myinfo-com' 14 | const AUTHORIZE_ENDPOINT = '/consent/oauth2/authorize' 15 | const CONSENT_TEMPLATE = fs.readFileSync( 16 | path.resolve(__dirname, '../../../static/html/consent.html'), 17 | 'utf8', 18 | ) 19 | 20 | const authorizations = {} 21 | 22 | const authorize = (redirectTo) => (req, res) => { 23 | const { client_id, redirect_uri, attributes, purpose, state } = req.query 24 | const relayStateParams = qs.stringify({ 25 | client_id, 26 | redirect_uri, 27 | state, 28 | purpose, 29 | scope: (attributes || '').replace(/,/g, ' '), 30 | realm: MYINFO_ASSERT_ENDPOINT, 31 | response_type: 'code', 32 | }) 33 | const relayState = `${AUTHORIZE_ENDPOINT}${encodeURIComponent( 34 | '?' + relayStateParams, 35 | )}` 36 | res.redirect(redirectTo(relayState)) 37 | } 38 | 39 | const authorizeViaOIDC = authorize( 40 | (state) => 41 | `/singpass/authorize?client_id=MYINFO-CONSENTPLATFORM&redirect_uri=${MYINFO_ASSERT_ENDPOINT}&state=${state}`, 42 | ) 43 | 44 | function config(app, { isStateless }) { 45 | app.get(MYINFO_ASSERT_ENDPOINT, (req, res) => { 46 | const rawArtifact = req.query.SAMLart || req.query.code 47 | const artifact = rawArtifact.replace(/ /g, '+') 48 | const state = req.query.RelayState || req.query.state 49 | 50 | const profile = lookUpByAuthCode(artifact, { isStateless }).profile 51 | const myinfoVersion = 'v3' 52 | 53 | const { nric: id } = profile 54 | 55 | const persona = assertions.myinfo[myinfoVersion].personas[id] 56 | if (!persona) { 57 | res.status(404).send({ 58 | message: 'Cannot find MyInfo Persona', 59 | artifact, 60 | myinfoVersion, 61 | id, 62 | }) 63 | } else { 64 | res.cookie('connect.sid', id) 65 | res.redirect(state) 66 | } 67 | }) 68 | 69 | app.get(AUTHORIZE_ENDPOINT, cookieParser(), (req, res) => { 70 | const params = { 71 | ...req.query, 72 | scope: req.query.scope.replace(/\+/g, ' '), 73 | id: req.cookies['connect.sid'], 74 | action: AUTHORIZE_ENDPOINT, 75 | } 76 | 77 | res.send(render(CONSENT_TEMPLATE, params)) 78 | }) 79 | 80 | app.post( 81 | AUTHORIZE_ENDPOINT, 82 | cookieParser(), 83 | express.urlencoded({ 84 | extended: false, 85 | type: 'application/x-www-form-urlencoded', 86 | }), 87 | (req, res) => { 88 | const id = req.cookies['connect.sid'] 89 | const code = uuid() 90 | authorizations[code] = [ 91 | { 92 | sub: id, 93 | auth_level: 0, 94 | scope: req.body.scope.split(' '), 95 | iss: `${req.protocol}://${req.get( 96 | 'host', 97 | )}/consent/oauth2/consent/myinfo-com`, 98 | tokenName: 'access_token', 99 | token_type: 'Bearer', 100 | authGrantId: code, 101 | auditTrackingId: code, 102 | jti: code, 103 | aud: 'myinfo', 104 | grant_type: 'authorization_code', 105 | realm: '/consent/myinfo-com', 106 | }, 107 | req.body.redirect_uri, 108 | ] 109 | const callbackParams = qs.stringify( 110 | req.body.decision === 'allow' 111 | ? { 112 | code, 113 | ...pick(req.body, ['state', 'scope', 'client_id']), 114 | iss: `${req.protocol}://${req.get( 115 | 'host', 116 | )}/consent/oauth2/consent/myinfo-com`, 117 | } 118 | : { 119 | state: req.body.state, 120 | 'error-description': 121 | 'Resource Owner did not authorize the request', 122 | error: 'access_denied', 123 | }, 124 | ) 125 | res.redirect(`${req.body.redirect_uri}?${callbackParams}`) 126 | }, 127 | ) 128 | 129 | return app 130 | } 131 | 132 | module.exports = { 133 | authorizeViaOIDC, 134 | authorizations, 135 | config, 136 | } 137 | -------------------------------------------------------------------------------- /lib/express/myinfo/controllers.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto') 2 | const fs = require('fs') 3 | const path = require('path') 4 | 5 | const express = require('express') 6 | const { pick, partition } = require('lodash') 7 | 8 | const jose = require('jose') 9 | const jwt = require('jsonwebtoken') 10 | 11 | const assertions = require('../../assertions') 12 | const consent = require('./consent') 13 | 14 | const MOCKPASS_PRIVATE_KEY = fs.readFileSync( 15 | path.resolve(__dirname, '../../../static/certs/spcp-key.pem'), 16 | ) 17 | const MOCKPASS_PUBLIC_KEY = fs.readFileSync( 18 | path.resolve(__dirname, '../../../static/certs/spcp.crt'), 19 | ) 20 | 21 | const MYINFO_SECRET = process.env.SERVICE_PROVIDER_MYINFO_SECRET 22 | 23 | module.exports = 24 | (version, myInfoSignature) => 25 | (app, { serviceProvider, encryptMyInfo }) => { 26 | const verify = (signature, baseString) => { 27 | const verifier = crypto.createVerify('RSA-SHA256') 28 | verifier.update(baseString) 29 | verifier.end() 30 | return verifier.verify(serviceProvider.pubKey, signature, 'base64') 31 | } 32 | 33 | const encryptPersona = async (persona) => { 34 | /* 35 | * We sign and encrypt the persona. It's important to note that although a signature is 36 | * usually derived from the payload hash and is thus much smaller than the payload itself, 37 | * we're specifically contructeding a JWT, which contains the original payload. 38 | * 39 | * We then construct a JWE and provide two headers specifying the encryption algorithms used. 40 | * You can read about them here: https://www.rfc-editor.org/rfc/inline-errata/rfc7518.html 41 | * 42 | * These values weren't picked arbitrarily; they were the defaults used by a library we 43 | * formerly used: node-jose. We opted to continue using them for backwards compatibility. 44 | */ 45 | const privateKey = await jose.importPKCS8(MOCKPASS_PRIVATE_KEY.toString()) 46 | const sign = await new jose.SignJWT(persona) 47 | .setProtectedHeader({ alg: 'RS256' }) 48 | .sign(privateKey) 49 | const publicKey = await jose.importX509(serviceProvider.cert.toString()) 50 | const encryptedAndSignedPersona = await new jose.CompactEncrypt( 51 | Buffer.from(sign), 52 | ) 53 | .setProtectedHeader({ alg: 'RSA-OAEP', enc: 'A256GCM' }) 54 | .encrypt(publicKey) 55 | return encryptedAndSignedPersona 56 | } 57 | 58 | const lookupPerson = (allowedAttributes) => async (req, res) => { 59 | const requestedAttributes = (req.query.attributes || '').split(',') 60 | 61 | const [attributes, disallowedAttributes] = partition( 62 | requestedAttributes, 63 | (v) => allowedAttributes.includes(v), 64 | ) 65 | 66 | if (disallowedAttributes.length > 0) { 67 | res.status(401).send({ 68 | code: 401, 69 | message: 'Disallowed', 70 | fields: disallowedAttributes.join(','), 71 | }) 72 | } else { 73 | const transformPersona = encryptMyInfo 74 | ? encryptPersona 75 | : (person) => person 76 | const persona = assertions.myinfo[version].personas[req.params.uinfin] 77 | res.status(persona ? 200 : 404).send( 78 | persona 79 | ? await transformPersona(pick(persona, attributes)) 80 | : { 81 | code: 404, 82 | message: 'UIN/FIN does not exist in MyInfo.', 83 | fields: '', 84 | }, 85 | ) 86 | } 87 | } 88 | 89 | const allowedAttributes = assertions.myinfo[version].attributes 90 | 91 | app.get( 92 | `/myinfo/${version}/person-basic/:uinfin/`, 93 | (req, res, next) => { 94 | // sp_esvcId and txnNo needed as query params 95 | const [, authHeader] = req.get('Authorization').split(' ') 96 | 97 | const { signature, baseString } = myInfoSignature(authHeader, req) 98 | if (verify(signature, baseString)) { 99 | next() 100 | } else { 101 | res.status(403).send({ 102 | code: 403, 103 | message: `Signature verification failed, ${baseString} does not result in ${signature}`, 104 | fields: '', 105 | }) 106 | } 107 | }, 108 | lookupPerson(allowedAttributes.basic), 109 | ) 110 | app.get(`/myinfo/${version}/person/:uinfin/`, (req, res) => { 111 | const authz = req.get('Authorization').split(' ') 112 | const token = authz.pop() 113 | 114 | const authHeader = (authz[1] || '').replace(',Bearer', '') 115 | const { signature, baseString } = encryptMyInfo 116 | ? myInfoSignature(authHeader, req) 117 | : {} 118 | 119 | const { sub, scope } = jwt.verify(token, MOCKPASS_PUBLIC_KEY, { 120 | algorithms: ['RS256'], 121 | }) 122 | if (encryptMyInfo && !verify(signature, baseString)) { 123 | res.status(401).send({ 124 | code: 401, 125 | message: `Signature verification failed, ${baseString} does not result in ${signature}`, 126 | }) 127 | } else if (sub !== req.params.uinfin) { 128 | res.status(401).send({ 129 | code: 401, 130 | message: 'UIN requested does not match logged in user', 131 | }) 132 | } else { 133 | lookupPerson(scope)(req, res) 134 | } 135 | }) 136 | 137 | app.get(`/myinfo/${version}/authorise`, consent.authorizeViaOIDC) 138 | 139 | app.post( 140 | `/myinfo/${version}/token`, 141 | express.urlencoded({ 142 | extended: false, 143 | type: 'application/x-www-form-urlencoded', 144 | }), 145 | (req, res) => { 146 | const [tokenTemplate, redirect_uri] = 147 | consent.authorizations[req.body.code] 148 | const [, authHeader] = (req.get('Authorization') || '').split(' ') 149 | 150 | const { signature, baseString } = MYINFO_SECRET 151 | ? myInfoSignature(authHeader, req, { 152 | client_secret: MYINFO_SECRET, 153 | redirect_uri, 154 | }) 155 | : {} 156 | if (!tokenTemplate) { 157 | res.status(400).send({ 158 | code: 400, 159 | message: 'No such authorization given', 160 | fields: '', 161 | }) 162 | } else if (MYINFO_SECRET && !verify(signature, baseString)) { 163 | res.status(403).send({ 164 | code: 403, 165 | message: `Signature verification failed, ${baseString} does not result in ${signature}`, 166 | }) 167 | } else { 168 | const token = jwt.sign( 169 | { ...tokenTemplate, auth_time: Date.now() }, 170 | MOCKPASS_PRIVATE_KEY, 171 | { expiresIn: '1800 seconds', algorithm: 'RS256' }, 172 | ) 173 | res.send({ 174 | access_token: token, 175 | token_type: 'Bearer', 176 | expires_in: 1798, 177 | }) 178 | } 179 | }, 180 | ) 181 | 182 | return app 183 | } 184 | -------------------------------------------------------------------------------- /lib/express/myinfo/index.js: -------------------------------------------------------------------------------- 1 | const { config: consent } = require('./consent') 2 | const controllers = require('./controllers') 3 | 4 | const { pki } = require('../../crypto/myinfo-signature') 5 | 6 | module.exports = { 7 | consent, 8 | v3: controllers('v3', pki), 9 | } 10 | -------------------------------------------------------------------------------- /lib/express/oidc/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | configOIDC: require('./spcp'), 3 | configOIDCv2: require('./v2-ndi'), 4 | } 5 | -------------------------------------------------------------------------------- /lib/express/oidc/spcp.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const fs = require('fs') 3 | const { render } = require('mustache') 4 | const jose = require('node-jose') 5 | const path = require('path') 6 | const ExpiryMap = require('expiry-map') 7 | 8 | const assertions = require('../../assertions') 9 | const { generateAuthCode, lookUpByAuthCode } = require('../../auth-code') 10 | const { 11 | buildAssertURL, 12 | idGenerator, 13 | customProfileFromHeaders, 14 | } = require('./utils') 15 | 16 | const LOGIN_TEMPLATE = fs.readFileSync( 17 | path.resolve(__dirname, '../../../static/html/login-page.html'), 18 | 'utf8', 19 | ) 20 | const REFRESH_TOKEN_TIMEOUT = 24 * 60 * 60 * 1000 21 | const profileStore = new ExpiryMap(REFRESH_TOKEN_TIMEOUT) 22 | 23 | const signingPem = fs.readFileSync( 24 | path.resolve(__dirname, '../../../static/certs/spcp-key.pem'), 25 | ) 26 | 27 | function config(app, { showLoginPage, serviceProvider, isStateless }) { 28 | for (const idp of ['singPass', 'corpPass']) { 29 | const profiles = assertions.oidc[idp] 30 | const defaultProfile = 31 | profiles.find((p) => p.nric === process.env.MOCKPASS_NRIC) || profiles[0] 32 | 33 | app.get(`/${idp.toLowerCase()}/authorize`, (req, res) => { 34 | const { redirect_uri: redirectURI, state, nonce } = req.query 35 | if (showLoginPage(req)) { 36 | const values = profiles.map((profile) => { 37 | const authCode = generateAuthCode({ profile, nonce }, { isStateless }) 38 | const assertURL = buildAssertURL(redirectURI, authCode, state) 39 | const id = idGenerator[idp](profile) 40 | return { id, assertURL } 41 | }) 42 | const response = render(LOGIN_TEMPLATE, { 43 | values, 44 | customProfileConfig: { 45 | endpoint: `/${idp.toLowerCase()}/authorize/custom-profile`, 46 | showUuid: true, 47 | showUen: idp === 'corpPass', 48 | redirectURI, 49 | state, 50 | nonce, 51 | }, 52 | }) 53 | res.send(response) 54 | } else { 55 | const profile = customProfileFromHeaders[idp](req) || defaultProfile 56 | const authCode = generateAuthCode({ profile, nonce }, { isStateless }) 57 | const assertURL = buildAssertURL(redirectURI, authCode, state) 58 | console.warn( 59 | `Redirecting login from ${req.query.client_id} to ${redirectURI}`, 60 | ) 61 | res.redirect(assertURL) 62 | } 63 | }) 64 | 65 | app.get(`/${idp.toLowerCase()}/authorize/custom-profile`, (req, res) => { 66 | const { nric, uuid, uen, redirectURI, state, nonce } = req.query 67 | 68 | const profile = { nric, uuid } 69 | if (idp === 'corpPass') { 70 | profile.name = `Name of ${nric}` 71 | profile.isSingPassHolder = false 72 | profile.uen = uen 73 | } 74 | 75 | const authCode = generateAuthCode({ profile, nonce }, { isStateless }) 76 | const assertURL = buildAssertURL(redirectURI, authCode, state) 77 | res.redirect(assertURL) 78 | }) 79 | 80 | app.post( 81 | `/${idp.toLowerCase()}/token`, 82 | express.urlencoded({ extended: false }), 83 | async (req, res) => { 84 | const { client_id: aud, grant_type: grant } = req.body 85 | let profile, nonce 86 | 87 | if (grant === 'refresh_token') { 88 | const { refresh_token: suppliedRefreshToken } = req.body 89 | console.warn(`Refreshing tokens with ${suppliedRefreshToken}`) 90 | 91 | profile = isStateless 92 | ? JSON.parse( 93 | Buffer.from(suppliedRefreshToken, 'base64url').toString( 94 | 'utf-8', 95 | ), 96 | ) 97 | : profileStore.get(suppliedRefreshToken) 98 | } else { 99 | const { code: authCode } = req.body 100 | console.warn( 101 | `Received auth code ${authCode} from ${aud} and ${req.body.redirect_uri}`, 102 | ) 103 | ;({ profile, nonce } = lookUpByAuthCode(authCode, { isStateless })) 104 | } 105 | 106 | const iss = `${req.protocol}://${req.get('host')}` 107 | 108 | const { 109 | idTokenClaims, 110 | accessToken, 111 | refreshToken: generatedRefreshToken, 112 | } = await assertions.oidc.create[idp](profile, iss, aud, nonce) 113 | 114 | const refreshToken = isStateless 115 | ? Buffer.from(JSON.stringify(profile)).toString('base64url') 116 | : generatedRefreshToken 117 | profileStore.set(refreshToken, profile) 118 | 119 | const signingKey = await jose.JWK.asKey(signingPem, 'pem') 120 | const signedIdToken = await jose.JWS.createSign( 121 | { format: 'compact' }, 122 | signingKey, 123 | ) 124 | .update(JSON.stringify(idTokenClaims)) 125 | .final() 126 | 127 | const encryptionKey = await jose.JWK.asKey(serviceProvider.cert, 'pem') 128 | const idToken = await jose.JWE.createEncrypt( 129 | { format: 'compact', fields: { cty: 'JWT' } }, 130 | encryptionKey, 131 | ) 132 | .update(signedIdToken) 133 | .final() 134 | 135 | res.send({ 136 | access_token: accessToken, 137 | refresh_token: refreshToken, 138 | expires_in: 24 * 60 * 60, 139 | scope: 'openid', 140 | token_type: 'bearer', 141 | id_token: idToken, 142 | }) 143 | }, 144 | ) 145 | } 146 | return app 147 | } 148 | 149 | module.exports = config 150 | -------------------------------------------------------------------------------- /lib/express/oidc/utils.js: -------------------------------------------------------------------------------- 1 | const assertions = require('../../assertions') 2 | 3 | const buildAssertURL = (redirectURI, authCode, state) => 4 | `${redirectURI}?code=${encodeURIComponent( 5 | authCode, 6 | )}&state=${encodeURIComponent(state)}` 7 | 8 | const idGenerator = { 9 | singPass: ({ nric }) => 10 | assertions.myinfo.v3.personas[nric] ? `${nric} [MyInfo]` : nric, 11 | corpPass: ({ nric, uen }) => `${nric} / UEN: ${uen}`, 12 | } 13 | 14 | const customProfileFromHeaders = { 15 | singPass: (req) => { 16 | const customNricHeader = req.header('X-Custom-NRIC') 17 | const customUuidHeader = req.header('X-Custom-UUID') 18 | if (!customNricHeader || !customUuidHeader) { 19 | return false 20 | } 21 | return { nric: customNricHeader, uuid: customUuidHeader } 22 | }, 23 | corpPass: (req) => { 24 | const customNricHeader = req.header('X-Custom-NRIC') 25 | const customUuidHeader = req.header('X-Custom-UUID') 26 | const customUenHeader = req.header('X-Custom-UEN') 27 | if (!customNricHeader || !customUuidHeader || !customUenHeader) { 28 | return false 29 | } 30 | return { 31 | nric: customNricHeader, 32 | uuid: customUuidHeader, 33 | uen: customUenHeader, 34 | } 35 | }, 36 | } 37 | 38 | module.exports = { 39 | buildAssertURL, 40 | idGenerator, 41 | customProfileFromHeaders, 42 | } 43 | -------------------------------------------------------------------------------- /lib/express/oidc/v2-ndi.js: -------------------------------------------------------------------------------- 1 | // This file implements NDI OIDC for Singpass authentication and Corppass OIDC 2 | // for Corppass authentication. 3 | 4 | const express = require('express') 5 | const fs = require('fs') 6 | const { render } = require('mustache') 7 | const jose = require('jose') 8 | const path = require('path') 9 | 10 | const assertions = require('../../assertions') 11 | const { generateAuthCode, lookUpByAuthCode } = require('../../auth-code') 12 | const { 13 | buildAssertURL, 14 | idGenerator, 15 | customProfileFromHeaders, 16 | } = require('./utils') 17 | 18 | const LOGIN_TEMPLATE = fs.readFileSync( 19 | path.resolve(__dirname, '../../../static/html/login-page.html'), 20 | 'utf8', 21 | ) 22 | 23 | const aspPublic = fs.readFileSync( 24 | path.resolve(__dirname, '../../../static/certs/oidc-v2-asp-public.json'), 25 | ) 26 | 27 | const aspSecret = fs.readFileSync( 28 | path.resolve(__dirname, '../../../static/certs/oidc-v2-asp-secret.json'), 29 | ) 30 | 31 | const rpPublic = fs.readFileSync( 32 | path.resolve(__dirname, '../../../static/certs/oidc-v2-rp-public.json'), 33 | ) 34 | 35 | const singpass_token_endpoint_auth_signing_alg_values_supported = [ 36 | 'ES256', 37 | 'ES384', 38 | 'ES512', 39 | ] 40 | 41 | const corppass_token_endpoint_auth_signing_alg_values_supported = ['ES256'] 42 | 43 | const token_endpoint_auth_signing_alg_values_supported = { 44 | singPass: singpass_token_endpoint_auth_signing_alg_values_supported, 45 | corpPass: corppass_token_endpoint_auth_signing_alg_values_supported, 46 | } 47 | 48 | const singpass_id_token_encryption_alg_values_supported = [ 49 | 'ECDH-ES+A256KW', 50 | 'ECDH-ES+A192KW', 51 | 'ECDH-ES+A128KW', 52 | 'RSA-OAEP-256', 53 | ] 54 | 55 | const corppass_id_token_encryption_alg_values_supported = ['ECDH-ES+A256KW'] 56 | 57 | const id_token_encryption_alg_values_supported = { 58 | singPass: singpass_id_token_encryption_alg_values_supported, 59 | corpPass: corppass_id_token_encryption_alg_values_supported, 60 | } 61 | 62 | function findEcdhEsEncryptionKey(jwks, crv, algs) { 63 | let encryptionKey = jwks.keys.find( 64 | (item) => 65 | item.use === 'enc' && 66 | item.kty === 'EC' && 67 | item.crv === crv && 68 | (!item.alg || 69 | (item.alg === 'ECDH-ES+A256KW' && 70 | algs.some((alg) => alg === item.alg))), 71 | ) 72 | if (encryptionKey) { 73 | return { 74 | ...encryptionKey, 75 | ...(!encryptionKey.alg ? { alg: 'ECDH-ES+A256KW' } : {}), 76 | } 77 | } 78 | encryptionKey = jwks.keys.find( 79 | (item) => 80 | item.use === 'enc' && 81 | item.kty === 'EC' && 82 | item.crv === crv && 83 | (!item.alg || 84 | (item.alg === 'ECDH-ES+A192KW' && 85 | algs.some((alg) => alg === item.alg))), 86 | ) 87 | if (encryptionKey) { 88 | return { 89 | ...encryptionKey, 90 | ...(!encryptionKey.alg ? { alg: 'ECDH-ES+A256KW' } : {}), 91 | } 92 | } 93 | encryptionKey = jwks.keys.find( 94 | (item) => 95 | item.use === 'enc' && 96 | item.kty === 'EC' && 97 | item.crv === crv && 98 | (!item.alg || 99 | (item.alg === 'ECDH-ES+A128KW' && 100 | algs.some((alg) => alg === item.alg))), 101 | ) 102 | if (encryptionKey) { 103 | return { 104 | ...encryptionKey, 105 | ...(!encryptionKey.alg ? { alg: 'ECDH-ES+A256KW' } : {}), 106 | } 107 | } 108 | return null 109 | } 110 | 111 | function findEncryptionKey(jwks, algs) { 112 | let encryptionKey = findEcdhEsEncryptionKey(jwks, 'P-521', algs) 113 | if (encryptionKey) { 114 | return encryptionKey 115 | } 116 | if (!encryptionKey) { 117 | encryptionKey = findEcdhEsEncryptionKey(jwks, 'P-384', algs) 118 | } 119 | if (encryptionKey) { 120 | return encryptionKey 121 | } 122 | if (!encryptionKey) { 123 | encryptionKey = findEcdhEsEncryptionKey(jwks, 'P-256', algs) 124 | } 125 | if (encryptionKey) { 126 | return encryptionKey 127 | } 128 | if (!encryptionKey) { 129 | encryptionKey = jwks.keys.find( 130 | (item) => 131 | item.use === 'enc' && 132 | item.kty === 'RSA' && 133 | (!item.alg || 134 | (item.alg === 'RSA-OAEP-256' && 135 | algs.some((alg) => alg === item.alg))), 136 | ) 137 | } 138 | if (encryptionKey) { 139 | return { ...encryptionKey, alg: 'RSA-OAEP-256' } 140 | } 141 | } 142 | 143 | function config(app, { showLoginPage, isStateless }) { 144 | for (const idp of ['singPass', 'corpPass']) { 145 | const profiles = assertions.oidc[idp] 146 | const defaultProfile = 147 | profiles.find((p) => p.nric === process.env.MOCKPASS_NRIC) || profiles[0] 148 | 149 | app.get(`/${idp.toLowerCase()}/v2/authorize`, (req, res) => { 150 | const { 151 | scope, 152 | response_type, 153 | client_id, 154 | redirect_uri: redirectURI, 155 | state, 156 | nonce, 157 | } = req.query 158 | 159 | if (scope !== 'openid') { 160 | return res.status(400).send({ 161 | error: 'invalid_scope', 162 | error_description: `Unknown scope ${scope}`, 163 | }) 164 | } 165 | if (response_type !== 'code') { 166 | return res.status(400).send({ 167 | error: 'unsupported_response_type', 168 | error_description: `Unknown response_type ${response_type}`, 169 | }) 170 | } 171 | if (!client_id) { 172 | return res.status(400).send({ 173 | error: 'invalid_request', 174 | error_description: 'Missing client_id', 175 | }) 176 | } 177 | if (!redirectURI) { 178 | return res.status(400).send({ 179 | error: 'invalid_request', 180 | error_description: 'Missing redirect_uri', 181 | }) 182 | } 183 | if (!nonce) { 184 | return res.status(400).send({ 185 | error: 'invalid_request', 186 | error_description: 'Missing nonce', 187 | }) 188 | } 189 | if (!state) { 190 | return res.status(400).send({ 191 | error: 'invalid_request', 192 | error_description: 'Missing state', 193 | }) 194 | } 195 | 196 | // Identical to OIDC v1 197 | if (showLoginPage(req)) { 198 | const values = profiles.map((profile) => { 199 | const authCode = generateAuthCode({ profile, nonce }, { isStateless }) 200 | const assertURL = buildAssertURL(redirectURI, authCode, state) 201 | const id = idGenerator[idp](profile) 202 | return { id, assertURL } 203 | }) 204 | const response = render(LOGIN_TEMPLATE, { 205 | values, 206 | customProfileConfig: { 207 | endpoint: `/${idp.toLowerCase()}/v2/authorize/custom-profile`, 208 | showUuid: true, 209 | showUen: idp === 'corpPass', 210 | redirectURI, 211 | state, 212 | nonce, 213 | }, 214 | }) 215 | res.send(response) 216 | } else { 217 | const profile = customProfileFromHeaders[idp](req) || defaultProfile 218 | const authCode = generateAuthCode({ profile, nonce }, { isStateless }) 219 | const assertURL = buildAssertURL(redirectURI, authCode, state) 220 | console.warn( 221 | `Redirecting login from ${req.query.client_id} to ${redirectURI}`, 222 | ) 223 | res.redirect(assertURL) 224 | } 225 | }) 226 | 227 | app.get(`/${idp.toLowerCase()}/v2/authorize/custom-profile`, (req, res) => { 228 | const { nric, uuid, uen, redirectURI, state, nonce } = req.query 229 | 230 | const profile = { nric, uuid } 231 | if (idp === 'corpPass') { 232 | profile.name = `Name of ${nric}` 233 | profile.isSingPassHolder = false 234 | profile.uen = uen 235 | } 236 | 237 | const authCode = generateAuthCode({ profile, nonce }, { isStateless }) 238 | const assertURL = buildAssertURL(redirectURI, authCode, state) 239 | res.redirect(assertURL) 240 | }) 241 | 242 | app.post( 243 | `/${idp.toLowerCase()}/v2/token`, 244 | express.urlencoded({ extended: false }), 245 | async (req, res) => { 246 | const { 247 | client_id, 248 | redirect_uri: redirectURI, 249 | grant_type, 250 | code: authCode, 251 | client_assertion_type, 252 | client_assertion: clientAssertion, 253 | } = req.body 254 | 255 | // Only SP requires client_id 256 | if (idp === 'singPass' && !client_id) { 257 | console.error('Missing client_id') 258 | return res.status(400).send({ 259 | error: 'invalid_request', 260 | error_description: 'Missing client_id', 261 | }) 262 | } 263 | if (!redirectURI) { 264 | console.error('Missing redirect_uri') 265 | return res.status(400).send({ 266 | error: 'invalid_request', 267 | error_description: 'Missing redirect_uri', 268 | }) 269 | } 270 | if (grant_type !== 'authorization_code') { 271 | console.error('Unknown grant_type', grant_type) 272 | return res.status(400).send({ 273 | error: 'unsupported_grant_type', 274 | error_description: `Unknown grant_type ${grant_type}`, 275 | }) 276 | } 277 | if (!authCode) { 278 | return res.status(400).send({ 279 | error: 'invalid_request', 280 | error_description: 'Missing code', 281 | }) 282 | } 283 | if ( 284 | client_assertion_type !== 285 | 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer' 286 | ) { 287 | console.error('Unknown client_assertion_type', client_assertion_type) 288 | return res.status(400).send({ 289 | error: 'invalid_request', 290 | error_description: `Unknown client_assertion_type ${client_assertion_type}`, 291 | }) 292 | } 293 | if (!clientAssertion) { 294 | console.error('Missing client_assertion') 295 | return res.status(400).send({ 296 | error: 'invalid_request', 297 | error_description: 'Missing client_assertion', 298 | }) 299 | } 300 | 301 | // Step 0: Get the RP keyset 302 | const rpJwksEndpoint = 303 | idp === 'singPass' 304 | ? process.env.SP_RP_JWKS_ENDPOINT 305 | : process.env.CP_RP_JWKS_ENDPOINT 306 | 307 | let rpKeysetString 308 | 309 | if (rpJwksEndpoint) { 310 | try { 311 | const rpKeysetResponse = await fetch(rpJwksEndpoint, { 312 | method: 'GET', 313 | }) 314 | rpKeysetString = await rpKeysetResponse.text() 315 | if (!rpKeysetResponse.ok) { 316 | throw new Error(rpKeysetString) 317 | } 318 | } catch (e) { 319 | console.error( 320 | 'Failed to fetch RP JWKS from', 321 | rpJwksEndpoint, 322 | e.message, 323 | ) 324 | return res.status(400).send({ 325 | error: 'invalid_client', 326 | error_description: `Failed to fetch RP JWKS from specified endpoint: ${e.message}`, 327 | }) 328 | } 329 | } else { 330 | // If the endpoint is not defined, default to the sample keyset we provided. 331 | rpKeysetString = rpPublic 332 | } 333 | 334 | let rpKeysetJson 335 | try { 336 | rpKeysetJson = JSON.parse(rpKeysetString) 337 | } catch (e) { 338 | console.error('Unable to parse RP keyset', e.message) 339 | return res.status(400).send({ 340 | error: 'invalid_client', 341 | error_description: `Unable to parse RP keyset: ${e.message}`, 342 | }) 343 | } 344 | 345 | const rpKeyset = jose.createLocalJWKSet(rpKeysetJson) 346 | // Step 0.5: Verify client assertion with RP signing key 347 | let clientAssertionResult 348 | try { 349 | clientAssertionResult = await jose.jwtVerify( 350 | clientAssertion, 351 | rpKeyset, 352 | ) 353 | } catch (e) { 354 | console.error( 355 | 'Unable to verify client_assertion', 356 | e.message, 357 | clientAssertion, 358 | ) 359 | return res.status(401).send({ 360 | error: 'invalid_client', 361 | error_description: `Unable to verify client_assertion: ${e.message}`, 362 | }) 363 | } 364 | 365 | const { payload: clientAssertionClaims, protectedHeader } = 366 | clientAssertionResult 367 | console.debug( 368 | 'Received client_assertion', 369 | clientAssertionClaims, 370 | protectedHeader, 371 | ) 372 | if ( 373 | !token_endpoint_auth_signing_alg_values_supported[idp].some( 374 | (item) => item === protectedHeader.alg, 375 | ) 376 | ) { 377 | console.warn( 378 | 'The client_assertion alg', 379 | protectedHeader.alg, 380 | 'does not meet required token_endpoint_auth_signing_alg_values_supported', 381 | token_endpoint_auth_signing_alg_values_supported[idp], 382 | ) 383 | } 384 | 385 | if (idp === 'singPass') { 386 | if (clientAssertionClaims['sub'] !== client_id) { 387 | console.error( 388 | 'Incorrect sub in client_assertion claims. Found', 389 | clientAssertionClaims['sub'], 390 | 'but should be', 391 | client_id, 392 | ) 393 | return res.status(401).send({ 394 | error: 'invalid_client', 395 | error_description: 'Incorrect sub in client_assertion claims', 396 | }) 397 | } 398 | } else { 399 | // Since client_id is not given for corpPass, sub claim is required in 400 | // order to get aud for id_token. 401 | if (!clientAssertionClaims['sub']) { 402 | console.error('Missing sub in client_assertion claims') 403 | return res.status(401).send({ 404 | error: 'invalid_client', 405 | error_description: 'Missing sub in client_assertion claims', 406 | }) 407 | } 408 | } 409 | 410 | // According to OIDC spec, asp must check the aud claim. 411 | const iss = `${req.protocol}://${req.get( 412 | 'host', 413 | )}/${idp.toLowerCase()}/v2` 414 | 415 | if (clientAssertionClaims['aud'] !== iss) { 416 | console.error( 417 | 'Incorrect aud in client_assertion claims. Found', 418 | clientAssertionClaims['aud'], 419 | 'but should be', 420 | iss, 421 | ) 422 | return res.status(401).send({ 423 | error: 'invalid_client', 424 | error_description: 'Incorrect aud in client_assertion claims', 425 | }) 426 | } 427 | 428 | // Step 1: Obtain profile for which the auth code requested data for 429 | const { profile, nonce } = lookUpByAuthCode(authCode, { isStateless }) 430 | 431 | // Step 2: Get ID token 432 | const aud = clientAssertionClaims['sub'] 433 | console.debug('Received token request', { 434 | code: authCode, 435 | client_id: aud, 436 | redirect_uri: redirectURI, 437 | }) 438 | 439 | const { idTokenClaims, accessToken } = await assertions.oidc.create[ 440 | idp 441 | ](profile, iss, aud, nonce) 442 | 443 | // Step 3: Sign ID token with ASP signing key 444 | const aspKeyset = JSON.parse(aspSecret) 445 | const aspSigningKey = aspKeyset.keys.find( 446 | (item) => 447 | item.use === 'sig' && item.kty === 'EC' && item.crv === 'P-256', 448 | ) 449 | if (!aspSigningKey) { 450 | console.error('No suitable signing key found', aspKeyset.keys) 451 | return res.status(400).send({ 452 | error: 'invalid_request', 453 | error_description: 'No suitable signing key found', 454 | }) 455 | } 456 | const signingKey = await jose.importJWK(aspSigningKey, 'ES256') 457 | const signedProtectedHeader = { 458 | alg: 'ES256', 459 | typ: 'JWT', 460 | kid: aspSigningKey.kid, 461 | } 462 | const signedIdToken = await new jose.CompactSign( 463 | new TextEncoder().encode(JSON.stringify(idTokenClaims)), 464 | ) 465 | .setProtectedHeader(signedProtectedHeader) 466 | .sign(signingKey) 467 | 468 | if ( 469 | process.env.SINGPASS_CLIENT_PROFILE === 'direct' || 470 | process.env.SINGPASS_CLIENT_PROFILE === 'bridge' 471 | ) 472 | return res.status(200).send({ 473 | access_token: accessToken, 474 | token_type: 'Bearer', 475 | id_token: signedIdToken, 476 | ...(idp === 'corpPass' 477 | ? { scope: 'openid', expires_in: 10 * 60 } 478 | : {}), 479 | }) 480 | 481 | // Step 4: Encrypt ID token with RP encryption key 482 | const rpEncryptionKey = findEncryptionKey( 483 | rpKeysetJson, 484 | id_token_encryption_alg_values_supported[idp], 485 | ) 486 | if (!rpEncryptionKey) { 487 | console.error('No suitable encryption key found', rpKeysetJson.keys) 488 | return res.status(400).send({ 489 | error: 'invalid_request', 490 | error_description: 'No suitable encryption key found', 491 | }) 492 | } 493 | console.debug('Using encryption key', rpEncryptionKey) 494 | const encryptedProtectedHeader = { 495 | alg: rpEncryptionKey.alg, 496 | typ: 'JWT', 497 | kid: rpEncryptionKey.kid, 498 | enc: 'A256CBC-HS512', 499 | cty: 'JWT', 500 | } 501 | const idToken = await new jose.CompactEncrypt( 502 | new TextEncoder().encode(signedIdToken), 503 | ) 504 | .setProtectedHeader(encryptedProtectedHeader) 505 | .encrypt(await jose.importJWK(rpEncryptionKey, rpEncryptionKey.alg)) 506 | 507 | console.debug('ID Token', idToken) 508 | // Step 5: Send token 509 | res.status(200).send({ 510 | access_token: accessToken, 511 | token_type: 'Bearer', 512 | id_token: idToken, 513 | ...(idp === 'corpPass' 514 | ? { scope: 'openid', expires_in: 10 * 60 } 515 | : {}), 516 | }) 517 | }, 518 | ) 519 | 520 | app.get( 521 | `/${idp.toLowerCase()}/v2/.well-known/openid-configuration`, 522 | (req, res) => { 523 | const baseUrl = `${req.protocol}://${req.get( 524 | 'host', 525 | )}/${idp.toLowerCase()}/v2` 526 | 527 | // Note: does not support backchannel auth 528 | const data = { 529 | issuer: baseUrl, 530 | authorization_endpoint: `${baseUrl}/authorize`, 531 | jwks_uri: `${baseUrl}/.well-known/keys`, 532 | response_types_supported: ['code'], 533 | scopes_supported: ['openid'], 534 | subject_types_supported: ['public'], 535 | claims_supported: ['nonce', 'aud', 'iss', 'sub', 'exp', 'iat'], 536 | grant_types_supported: ['authorization_code'], 537 | token_endpoint: `${baseUrl}/token`, 538 | token_endpoint_auth_methods_supported: ['private_key_jwt'], 539 | token_endpoint_auth_signing_alg_values_supported: 540 | token_endpoint_auth_signing_alg_values_supported[idp], 541 | id_token_signing_alg_values_supported: ['ES256'], 542 | id_token_encryption_alg_values_supported: 543 | id_token_encryption_alg_values_supported[idp], 544 | id_token_encryption_enc_values_supported: ['A256CBC-HS512'], 545 | } 546 | 547 | if (idp === 'corpPass') { 548 | data['claims_supported'] = [ 549 | ...data['claims_supported'], 550 | 'userInfo', 551 | 'EntityInfo', 552 | 'rt_hash', 553 | 'at_hash', 554 | 'amr', 555 | ] 556 | // Omit authorization-info_endpoint for CP 557 | } 558 | 559 | res.status(200).send(data) 560 | }, 561 | ) 562 | 563 | app.get(`/${idp.toLowerCase()}/v2/.well-known/keys`, (req, res) => { 564 | res.status(200).send(JSON.parse(aspPublic)) 565 | }) 566 | } 567 | return app 568 | } 569 | 570 | module.exports = config 571 | -------------------------------------------------------------------------------- /lib/express/sgid.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const fs = require('fs') 3 | const { render } = require('mustache') 4 | const jose = require('node-jose') 5 | const path = require('path') 6 | 7 | const assertions = require('../assertions') 8 | const { generateAuthCode, lookUpByAuthCode } = require('../auth-code') 9 | 10 | const LOGIN_TEMPLATE = fs.readFileSync( 11 | path.resolve(__dirname, '../../static/html/login-page.html'), 12 | 'utf8', 13 | ) 14 | 15 | const VERSION_PREFIX = '/v2' 16 | const OAUTH_PREFIX = '/oauth' 17 | const PATH_PREFIX = VERSION_PREFIX + OAUTH_PREFIX 18 | 19 | const signingPem = fs.readFileSync( 20 | path.resolve(__dirname, '../../static/certs/spcp-key.pem'), 21 | ) 22 | 23 | const idGenerator = { 24 | singPass: ({ nric }) => 25 | assertions.myinfo.v3.personas[nric] ? `${nric} [MyInfo]` : nric, 26 | } 27 | 28 | const buildAssertURL = (redirectURI, authCode, state) => 29 | `${redirectURI}?code=${encodeURIComponent( 30 | authCode, 31 | )}&state=${encodeURIComponent(state)}` 32 | 33 | function config(app, { showLoginPage, serviceProvider, isStateless }) { 34 | const profiles = assertions.oidc.singPass 35 | const defaultProfile = 36 | profiles.find((p) => p.nric === process.env.MOCKPASS_NRIC) || profiles[0] 37 | 38 | app.get(`${PATH_PREFIX}/authorize`, (req, res) => { 39 | const { redirect_uri: redirectURI, state, nonce } = req.query 40 | const scopes = req.query.scope ?? 'openid' 41 | console.info(`Requested scope ${scopes}`) 42 | if (showLoginPage(req)) { 43 | const values = profiles 44 | .filter((profile) => assertions.myinfo.v3.personas[profile.nric]) 45 | .map((profile) => { 46 | const authCode = generateAuthCode( 47 | { profile, scopes, nonce }, 48 | { isStateless }, 49 | ) 50 | const assertURL = buildAssertURL(redirectURI, authCode, state) 51 | const id = idGenerator.singPass(profile) 52 | return { id, assertURL } 53 | }) 54 | const response = render(LOGIN_TEMPLATE, { values }) 55 | res.send(response) 56 | } else { 57 | const profile = defaultProfile 58 | const authCode = generateAuthCode( 59 | { profile, scopes, nonce }, 60 | { isStateless }, 61 | ) 62 | const assertURL = buildAssertURL(redirectURI, authCode, state) 63 | console.info( 64 | `Redirecting login from ${req.query.client_id} to ${assertURL}`, 65 | ) 66 | res.redirect(assertURL) 67 | } 68 | }) 69 | 70 | app.post( 71 | `${PATH_PREFIX}/token`, 72 | express.json(), 73 | express.urlencoded({ extended: true }), 74 | async (req, res) => { 75 | console.log(req.body) 76 | const { client_id: aud, code: authCode } = req.body 77 | 78 | console.info( 79 | `Received auth code ${authCode} from ${aud} and ${req.body.redirect_uri}`, 80 | ) 81 | 82 | try { 83 | const { profile, scopes, nonce } = lookUpByAuthCode(authCode, { 84 | isStateless, 85 | }) 86 | console.info( 87 | `Profile ${JSON.stringify(profile)} with token scope ${scopes}`, 88 | ) 89 | const accessToken = authCode 90 | const iss = `${req.protocol}://${req.get('host') + VERSION_PREFIX}` 91 | 92 | const { idTokenClaims, refreshToken } = assertions.oidc.create.singPass( 93 | profile, 94 | iss, 95 | aud, 96 | nonce, 97 | accessToken, 98 | ) 99 | // Change sub from `s=${nric},u=${uuid}` 100 | // to `u=${uuid}` to be consistent with userinfo sub 101 | idTokenClaims.sub = idTokenClaims.sub.split(',')[1] 102 | 103 | const signingKey = await jose.JWK.asKey(signingPem, 'pem') 104 | const idToken = await jose.JWS.createSign( 105 | { format: 'compact' }, 106 | signingKey, 107 | ) 108 | .update(JSON.stringify(idTokenClaims)) 109 | .final() 110 | 111 | res.json({ 112 | access_token: accessToken, 113 | refresh_token: refreshToken, 114 | expires_in: 24 * 60 * 60, 115 | scope: scopes, 116 | token_type: 'Bearer', 117 | id_token: idToken, 118 | }) 119 | } catch (error) { 120 | console.error(error) 121 | res.status(500).json({ message: error.message }) 122 | } 123 | }, 124 | ) 125 | 126 | app.get(`${PATH_PREFIX}/userinfo`, async (req, res) => { 127 | const authCode = ( 128 | req.headers.authorization || req.headers.Authorization 129 | ).replace('Bearer ', '') 130 | // eslint-disable-next-line no-unused-vars 131 | const { profile, scopes, unused } = lookUpByAuthCode(authCode, { 132 | isStateless, 133 | }) 134 | const uuid = profile.uuid 135 | const nric = assertions.oidc.singPass.find((p) => p.uuid === uuid).nric 136 | const persona = assertions.myinfo.v3.personas[nric] 137 | 138 | console.info(`userinfo scopes ${scopes}`) 139 | const payloadKey = await jose.JWK.createKey('oct', 256, { 140 | alg: 'A256GCM', 141 | }) 142 | 143 | const encryptPayload = async (field) => { 144 | return await jose.JWE.createEncrypt({ format: 'compact' }, payloadKey) 145 | .update(field) 146 | .final() 147 | } 148 | const encryptedNric = await encryptPayload(nric) 149 | // sgID doesn't actually offer the openid scope yet 150 | const scopesArr = scopes 151 | .split(' ') 152 | .filter((field) => field !== 'openid' && field !== 'myinfo.nric_number') 153 | console.info(`userinfo scopesArr ${scopesArr}`) 154 | const myInfoFields = await Promise.all( 155 | scopesArr.map((scope) => 156 | encryptPayload(sgIDScopeToMyInfoField(persona, scope)), 157 | ), 158 | ) 159 | 160 | const data = {} 161 | scopesArr.forEach((name, index) => { 162 | data[name] = myInfoFields[index] 163 | }) 164 | data['myinfo.nric_number'] = encryptedNric 165 | const encryptionKey = await jose.JWK.asKey(serviceProvider.pubKey, 'pem') 166 | 167 | const plaintextPayloadKey = JSON.stringify(payloadKey.toJSON(true)) 168 | const encryptedPayloadKey = await jose.JWE.createEncrypt( 169 | { format: 'compact' }, 170 | encryptionKey, 171 | ) 172 | .update(plaintextPayloadKey) 173 | .final() 174 | res.json({ 175 | sub: `u=${uuid}`, 176 | key: encryptedPayloadKey, 177 | data, 178 | }) 179 | }) 180 | 181 | app.get(`${VERSION_PREFIX}/.well-known/jwks.json`, async (_req, res) => { 182 | const key = await jose.JWK.asKey(signingPem, 'pem') 183 | const jwk = key.toJSON() 184 | jwk.use = 'sig' 185 | res.json({ keys: [jwk] }) 186 | }) 187 | 188 | app.get( 189 | `${VERSION_PREFIX}/.well-known/openid-configuration`, 190 | async (req, res) => { 191 | const issuer = `${req.protocol}://${req.get('host') + VERSION_PREFIX}` 192 | 193 | res.json({ 194 | issuer, 195 | authorization_endpoint: `${issuer}/${OAUTH_PREFIX}/authorize`, 196 | token_endpoint: `${issuer}/${OAUTH_PREFIX}/token`, 197 | userinfo_endpoint: `${issuer}/${OAUTH_PREFIX}/userinfo`, 198 | jwks_uri: `${issuer}/.well-known/jwks.json`, 199 | response_types_supported: ['code'], 200 | grant_types_supported: ['authorization_code'], 201 | // Note: some of these scopes are not yet officially documented 202 | // in https://docs.id.gov.sg/data-catalog 203 | // So they are not officially supported yet. 204 | scopes_supported: [ 205 | 'openid', 206 | 'myinfo.nric_number', 207 | 'myinfo.name', 208 | 'myinfo.email', 209 | 'myinfo.sex', 210 | 'myinfo.race', 211 | 'myinfo.mobile_number', 212 | 'myinfo.registered_address', 213 | 'myinfo.date_of_birth', 214 | 'myinfo.passport_number', 215 | 'myinfo.passport_expiry_date', 216 | 'myinfo.nationality', 217 | 'myinfo.residentialstatus', 218 | 'myinfo.residential', 219 | 'myinfo.housingtype', 220 | 'myinfo.hdbtype', 221 | 'myinfo.birth_country', 222 | 'myinfo.vehicles', 223 | 'myinfo.name_of_employer', 224 | 'myinfo.workpass_status', 225 | 'myinfo.workpass_expiry_date', 226 | 'myinfo.marital_status', 227 | 'myinfo.mobile_number_with_country_code', 228 | ], 229 | id_token_signing_alg_values_supported: ['RS256'], 230 | subject_types_supported: ['pairwise'], 231 | }) 232 | }, 233 | ) 234 | } 235 | 236 | const concatMyInfoRegAddr = (regadd) => { 237 | const line1 = 238 | !!regadd.block.value || !!regadd.street.value 239 | ? `${regadd.block.value} ${regadd.street.value}` 240 | : '' 241 | const line2 = 242 | !!regadd.floor.value || !!regadd.unit.value 243 | ? `#${regadd.floor.value}-${regadd.unit.value}` 244 | : '' 245 | const line3 = 246 | !!regadd.country.desc || !!regadd.postal.value 247 | ? `${regadd.country.desc} ${regadd.postal.value}` 248 | : '' 249 | return `${line1}\n${line2}\n${line3}` 250 | } 251 | 252 | // Refer to sgid myinfo parser 253 | const formatMobileNumberWithPrefix = (phone) => { 254 | if (!phone || !phone.nbr?.value) { 255 | return 'NA' 256 | } 257 | return phone.prefix?.value && phone.areacode?.value 258 | ? `${phone.prefix?.value}${phone.areacode?.value} ${phone.nbr?.value}` 259 | : phone.nbr?.value 260 | } 261 | 262 | // Refer to sgid myinfo parser 263 | const formatVehicles = (vehicles) => { 264 | const vehicleObjects = 265 | vehicles?.map((vehicle) => ({ 266 | vehicle_number: vehicle.vehicleno?.value || 'NA', 267 | })) || '[]' 268 | return vehicleObjects 269 | } 270 | 271 | const formatJsonStringify = (value) => { 272 | return value == undefined ? 'NA' : JSON.stringify(value) 273 | } 274 | 275 | const defaultUndefinedToNA = (value) => { 276 | return value || 'NA' 277 | } 278 | 279 | // Refer to https://docs.id.gov.sg/data-catalog 280 | const sgIDScopeToMyInfoField = (persona, scope) => { 281 | switch (scope) { 282 | // No NRIC as that is always returned by default 283 | case 'openid': 284 | return defaultUndefinedToNA(persona.uuid?.value) 285 | case 'myinfo.name': 286 | return defaultUndefinedToNA(persona.name?.value) 287 | case 'myinfo.email': 288 | return defaultUndefinedToNA(persona.email?.value) 289 | case 'myinfo.sex': 290 | return defaultUndefinedToNA(persona.sex?.desc) 291 | case 'myinfo.race': 292 | return defaultUndefinedToNA(persona.race?.desc) 293 | case 'myinfo.mobile_number': 294 | return defaultUndefinedToNA(persona.mobileno?.nbr?.value) 295 | case 'myinfo.registered_address': 296 | return concatMyInfoRegAddr(persona.regadd) 297 | case 'myinfo.date_of_birth': 298 | return defaultUndefinedToNA(persona.dob?.value) 299 | case 'myinfo.passport_number': 300 | return defaultUndefinedToNA(persona.passportnumber?.value) 301 | case 'myinfo.passport_expiry_date': 302 | return defaultUndefinedToNA(persona.passportexpirydate?.value) 303 | case 'myinfo.nationality': 304 | return defaultUndefinedToNA(persona.nationality?.desc) 305 | case 'myinfo.residentialstatus': 306 | return defaultUndefinedToNA(persona.residentialstatus?.desc) 307 | case 'myinfo.residential': 308 | return defaultUndefinedToNA(persona.residential?.desc) 309 | case 'myinfo.housingtype': 310 | return defaultUndefinedToNA(persona.housingtype?.desc) 311 | case 'myinfo.hdbtype': 312 | return defaultUndefinedToNA(persona.hdbtype?.desc) 313 | case 'myinfo.birth_country': 314 | return defaultUndefinedToNA(persona.birthcountry?.desc) 315 | case 'myinfo.vehicles': 316 | return formatVehicles(persona.vehicles) 317 | case 'myinfo.name_of_employer': 318 | return defaultUndefinedToNA(persona.employment?.value) 319 | case 'myinfo.workpass_status': 320 | return defaultUndefinedToNA(persona.passstatus?.value) 321 | case 'myinfo.workpass_expiry_date': 322 | return defaultUndefinedToNA(persona.passexpirydate?.value) 323 | case 'myinfo.marital_status': 324 | return defaultUndefinedToNA(persona.marital?.desc) 325 | case 'myinfo.mobile_number_with_country_code': 326 | return formatMobileNumberWithPrefix(persona.mobileno) 327 | case 'pocdex.public_officer_details': 328 | return formatJsonStringify(persona.publicofficerdetails) 329 | default: 330 | return 'NA' 331 | } 332 | } 333 | 334 | module.exports = config 335 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@opengovsg/mockpass", 3 | "version": "4.3.5", 4 | "description": "A mock SingPass/CorpPass server for dev purposes", 5 | "main": "app.js", 6 | "bin": { 7 | "mockpass": "index.js" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1", 11 | "start": "nodemon index", 12 | "cz": "git-cz", 13 | "lint": "eslint lib", 14 | "lint-fix": "eslint --fix lib", 15 | "prepare": "node .husky/install.mjs", 16 | "prepublishOnly": "pinst --disable", 17 | "postpublish": "pinst --enable" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/opengovsg/mockpass.git" 22 | }, 23 | "keywords": [ 24 | "mock", 25 | "test", 26 | "singpass", 27 | "corppass" 28 | ], 29 | "author": "Government Technology Agency of Singapore (https://www.tech.gov.sg)", 30 | "license": "MIT", 31 | "bugs": { 32 | "url": "https://github.com/opengovsg/mockpass/issues" 33 | }, 34 | "homepage": "https://github.com/opengovsg/mockpass#readme", 35 | "engines": { 36 | "node": ">=8.0.0" 37 | }, 38 | "dependencies": { 39 | "base-64": "^1.0.0", 40 | "cookie-parser": "^1.4.3", 41 | "dotenv": "^16.0.0", 42 | "expiry-map": "^2.0.0", 43 | "express": "^5.1.0", 44 | "jose": "^5.2.3", 45 | "jsonwebtoken": "^9.0.0", 46 | "lodash": "^4.17.11", 47 | "morgan": "^1.9.1", 48 | "mustache": "^4.2.0", 49 | "node-jose": "^2.0.0", 50 | "uuid": "^9.0.0" 51 | }, 52 | "devDependencies": { 53 | "@commitlint/cli": "^19.1.0", 54 | "@commitlint/config-conventional": "^19.0.3", 55 | "@commitlint/travis-cli": "^19.0.3", 56 | "@eslint/eslintrc": "^3.1.0", 57 | "@eslint/js": "^9.8.0", 58 | "commitizen": "^4.2.4", 59 | "cz-conventional-changelog": "^3.2.0", 60 | "eslint": "^9.8.0", 61 | "eslint-config-prettier": "^9.1.0", 62 | "eslint-plugin-prettier": "^4.0.0", 63 | "globals": "^16.0.0", 64 | "husky": "^9.0.11", 65 | "lint-staged": "^15.2.2", 66 | "nodemon": "^3.0.1", 67 | "pinst": "^3.0.0", 68 | "prettier": "^2.0.5" 69 | }, 70 | "lint-staged": { 71 | "**/*.(js|jsx|ts|tsx)": [ 72 | "eslint --fix" 73 | ] 74 | }, 75 | "config": { 76 | "commitizen": { 77 | "path": "./node_modules/cz-conventional-changelog" 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /public/mockpass/resources/css/animate.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | 3 | /* login page, qr tab tooltip animation */ 4 | @keyframes tooltip-ani { 5 | 0% { 6 | transform: translateX(0) rotateY(-16deg); 7 | opacity: 1; 8 | } 9 | 5% { 10 | transform: translateX(0) rotateY(20deg); 11 | opacity: 1; 12 | } 13 | 10% { 14 | transform: translateX(0) rotateY(-12deg); 15 | opacity: 1; 16 | } 17 | 15% { 18 | transform: translateX(0) rotateY(6deg); 19 | opacity: 1; 20 | } 21 | 20% { 22 | transform: translateX(0) rotateY(-4deg); 23 | opacity: 1; 24 | } 25 | 25% { 26 | transform: translateX(0) rotateY(1deg); 27 | opacity: 1; 28 | } 29 | 30% { 30 | transform: translateX(0) rotateY(0deg); 31 | opacity: 1; 32 | } 33 | 100% { 34 | transform: translateX(0) rotateY(0deg); 35 | opacity: 1; 36 | } 37 | } 38 | .ani-rotate { 39 | animation-name: tooltip-ani; 40 | animation-duration: 1.5s; 41 | animation-iteration-count: 3; 42 | animation-timing-function: cubic-bezier(1.000, 0.000, 0.000, 1.000); 43 | } -------------------------------------------------------------------------------- /public/mockpass/resources/css/common.css: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------ 2 | LOADING SCREEN START 3 | ------------------------------------------------ */ 4 | .loading-screen-wrappper { 5 | background-color: #000; 6 | position: fixed; 7 | height: 100%; 8 | width: 100vw; 9 | left: 0; 10 | top: 0; 11 | opacity: 0.7; 12 | z-index: 9999; 13 | } 14 | 15 | .loading-screen-container { 16 | position: relative; 17 | top: 50%; 18 | transform: translateY(-50%); 19 | } 20 | 21 | .loader-title { 22 | width: 300px; 23 | height: 50px; 24 | color: #fff; 25 | font-weight: bold; 26 | font-size: 16px; 27 | margin: auto; 28 | text-align: center; 29 | } 30 | 31 | .loader { 32 | border: 16px solid #f3f3f3; 33 | border-radius: 50%; 34 | border-top: 16px solid #ff0000; 35 | width: 50px; 36 | height: 50px; 37 | -webkit-animation: spin 2s linear infinite; 38 | animation: spin 2s linear infinite; 39 | margin: auto; 40 | } 41 | 42 | @-webkit-keyframes spin { 43 | 0% { -webkit-transform: rotate(0deg); } 44 | 100% { -webkit-transform: rotate(360deg); } 45 | } 46 | 47 | @keyframes spin { 48 | 0% { transform: rotate(0deg); } 49 | 100% { transform: rotate(360deg); } 50 | } 51 | 52 | /*------------------------------------------------ 53 | LOADING SCREEN END 54 | ------------------------------------------------ */ 55 | 56 | /*------------------------------------------------ 57 | PASSWORD COMPLEXITY START 58 | ------------------------------------------------ */ 59 | .password-complexity-checker-wrapper { 60 | position: relative; 61 | } 62 | 63 | .pwd-complexity-hidden { 64 | display: none; 65 | } 66 | 67 | .pwd-complexity-info { 68 | border: 1px solid #a4a4a4; 69 | background-color: #fff; 70 | position: absolute; 71 | z-index: 61; 72 | font-size: 14px; 73 | padding: 5px 10px; 74 | width: 100%; 75 | } 76 | 77 | .pc-form-success { 78 | color: #2fa13e; 79 | padding-right: 3px; 80 | font-size: inherit; 81 | vertical-align: top; 82 | } 83 | 84 | .pc-form-error { 85 | color: #cf2010; 86 | padding-right: 3px; 87 | font-size: inherit; 88 | vertical-align: top; 89 | } 90 | 91 | .pc-form-error .icon-exclamation, .pc-form-success .icon-exclamation { 92 | display: inline-block; 93 | width: 16px; 94 | height: 16px; 95 | margin-right: 10px; 96 | position: relative; 97 | top: 4px; 98 | background: url("../img/pwd-complexity-icon-0a8ed77b6b99b6fd7cf2206943de1612.png"); 99 | } 100 | 101 | .pc-form-success .icon-exclamation { 102 | background-position: -16px 0; 103 | } 104 | 105 | .pwd-complexity-info>p { 106 | margin: 0 0 5px 0; 107 | } 108 | 109 | .pwd-complexity-info>ul { 110 | list-style: none; 111 | margin: 0; 112 | padding: 0 0 10px 0; 113 | font-size: 12px; 114 | } 115 | 116 | input.password-error { 117 | border: 1px solid red; 118 | } 119 | /*------------------------------------------------ 120 | PASSWORD COMPLEXITY END 121 | ------------------------------------------------ */ -------------------------------------------------------------------------------- /public/mockpass/resources/css/reset.css: -------------------------------------------------------------------------------- 1 | html, body, div, span, object, iframe, 2 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 3 | abbr, address, cite, code, 4 | del, dfn, em, img, ins, kbd, q, samp, 5 | small, strong, sub, sup, var, 6 | b, i, 7 | dl, dt, dd, ol, ul, li, 8 | fieldset, form, label, legend, 9 | table, caption, tbody, tfoot, thead, tr, th, td, 10 | article, aside, canvas, details, figcaption, figure, 11 | footer, header, hgroup, menu, nav, section, summary, 12 | time, mark, audio, video { 13 | margin:0; 14 | padding:0; 15 | border:0; 16 | outline:0; 17 | font-size:100%; 18 | vertical-align:baseline; 19 | background:transparent; 20 | } 21 | 22 | body { 23 | line-height:1; 24 | } 25 | 26 | article,aside,details,figcaption,figure, 27 | footer,header,hgroup,menu,nav,section { 28 | display:block; 29 | } 30 | 31 | nav ul { 32 | list-style:none; 33 | } 34 | 35 | blockquote, q { 36 | quotes:none; 37 | } 38 | 39 | blockquote:before, blockquote:after, 40 | q:before, q:after { 41 | content:''; 42 | content:none; 43 | } 44 | 45 | a { 46 | margin:0; 47 | padding:0; 48 | font-size:100%; 49 | vertical-align:baseline; 50 | background:transparent; 51 | } 52 | 53 | /* change colours to suit your needs */ 54 | ins { 55 | background-color:#ff9; 56 | color:#000; 57 | text-decoration:none; 58 | } 59 | 60 | /* change colours to suit your needs */ 61 | mark { 62 | background-color:#ff9; 63 | color:#000; 64 | font-style:italic; 65 | font-weight:bold; 66 | } 67 | 68 | 69 | del { 70 | text-decoration: line-through; 71 | } 72 | 73 | abbr[title], dfn[title] { 74 | border-bottom:1px dotted; 75 | cursor:help; 76 | } 77 | 78 | table { 79 | border-collapse:collapse; 80 | border-spacing:0; 81 | } 82 | 83 | /* change border colour to suit your needs */ 84 | hr { 85 | display:block; 86 | height:1px; 87 | border:0; 88 | border-top:1px solid #cccccc; 89 | margin:1em 0; 90 | padding:0; 91 | } 92 | 93 | input, select { 94 | vertical-align:middle; 95 | } -------------------------------------------------------------------------------- /public/mockpass/resources/css/style-baseline-small-media.css: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------ 2 | Base Build Small Devices Common START 3 | ------------------------------------------------ */ 4 | @media only screen and (min-width: 320px) and (max-width: 767px) { 5 | /* --- Mobile Header Start --- */ 6 | #mobile-header { 7 | transition: margin-left .5s; 8 | padding: 16px 20px; 9 | height: 66px; 10 | width: 100%; 11 | border-bottom: 2px solid #E11F26; 12 | background-color: #fff; 13 | } 14 | .mobile-mockpass-logo { 15 | position: relative; 16 | top: -5px; 17 | height: 49px; 18 | padding: 0; 19 | width: 122px; 20 | background-size: contain!important; 21 | background: url("../../resources/img/logo/mockpass-logo.png"); 22 | background-repeat: no-repeat; 23 | margin: 0 auto; 24 | } 25 | /* --- Mobile Header End --- */ 26 | 27 | /* --- Main Navigation Start --- */ 28 | .mNavigation_container { 29 | width: 100%; 30 | } 31 | .mNavigation_body p { 32 | padding: 18px 5px; 33 | margin: 0px; 34 | } 35 | .mNavigation_title { 36 | background-color: #111; 37 | margin: 0 0 1px 0; 38 | text-decoration: none; 39 | color: #fff; 40 | display: block; 41 | border-bottom: 1px solid #17202A; 42 | } 43 | .mNavigation_title.mDropdown { 44 | padding: 8px 8px 8px 32px; 45 | font-size: 1.3em; 46 | } 47 | .mNavigation_body { 48 | background: lightgray; 49 | padding: 0px 8px 0px 5px; 50 | text-decoration: none; 51 | font-size: .85em; 52 | color: #fff; 53 | display: block; 54 | transition: .5s; 55 | background-color: #17202A; 56 | } 57 | .mNavigation_body>a { 58 | border-bottom: 1px solid #212F3D; 59 | font-size: 1.1em; 60 | } 61 | .plusminus { 62 | float: right; 63 | position: relative; 64 | } 65 | .mDropbtn { 66 | color: #fff; 67 | padding: 0; 68 | font-size: 1.5em; 69 | border: none; 70 | cursor: pointer; 71 | width: 100%; 72 | text-align: left; 73 | text-indent: 32px; 74 | background-color: #111; 75 | height: 40px; 76 | } 77 | .mDropdown-container { 78 | position: relative; 79 | display: inline-block; 80 | width: 100%; 81 | } 82 | .mDropdown-content { 83 | display: none; 84 | position: relative; 85 | background-color: #17202A; 86 | min-width: 160px; 87 | box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.2); 88 | z-index: 1; 89 | font-size: .7em; 90 | text-indent: 7px; 91 | line-height: 20px; 92 | } 93 | .mDropdown-content a { 94 | color: black; 95 | padding: 12px 16px; 96 | text-decoration: none; 97 | display: block; 98 | } 99 | .mDropdown-content a:hover { 100 | background-color: #212F3D; 101 | } 102 | .mDropdown-container:hover .mDropdown-content { 103 | display: block; 104 | } 105 | .mDropdown-container:hover .mDropbtn { 106 | background-color: none; 107 | } 108 | /* --- Main Navigation End --- */ 109 | 110 | /* --- Side Navigation Start --- */ 111 | .sidenav { 112 | height: 100%; 113 | width: 0; 114 | position: fixed; 115 | z-index: 100; 116 | top: 0; 117 | left: 0; 118 | background-color: #111; 119 | overflow-x: hidden; 120 | transition: .5s; 121 | padding-top: 15px; 122 | line-height: 30px; 123 | } 124 | .sidenav a { 125 | padding: 8px 8px 8px 32px; 126 | text-decoration: none; 127 | font-size: 1.3em; 128 | color: #fff; 129 | display: block; 130 | transition: .3s; 131 | } 132 | .sidenav a:hover { 133 | color: #f1f1f1; 134 | } 135 | .sidenav .closebtn { 136 | position: absolute; 137 | top: 10px; 138 | right: 25px; 139 | font-size: 2.57em; 140 | margin-left: 50px; 141 | color: #c0c0c0; 142 | } 143 | /* --- Side Navigation End --- */ 144 | 145 | /* --- Main Footer --- */ 146 | .container-wrapper-footer { 147 | color: #fff; 148 | background-color: #e2dedf; 149 | text-align: center; 150 | padding: 10px 0 10px; 151 | margin: auto; 152 | position: absolute; 153 | height: 69px; 154 | } 155 | .footer-terms-condition { 156 | text-align: center !important; 157 | font-size: 0.75em; 158 | color: #000; } 159 | .footer-terms-condition a { 160 | color: #000; } 161 | .footer-copyright { 162 | text-align: center !important; 163 | padding: 5px 0 0 0; 164 | font-size: 0.75em; 165 | color: #000; 166 | font-weight: normal; 167 | color: #000; } 168 | /* --- Main Footer End --- */ 169 | 170 | .commonbody-container { 171 | width: 100%; 172 | margin: 0 auto 25px auto; 173 | padding: 0 20px; 174 | } 175 | .inner-body-wrapper { 176 | padding-bottom: 69px; 177 | position: relative; 178 | } 179 | button.btn-cancel { 180 | position: relative; 181 | width: 100%; 182 | top: 55px; 183 | padding: 10px 0px; 184 | font-size: 1em; 185 | } 186 | button.btn-primary { 187 | position:relative; 188 | width: 100%; 189 | top: -50px; 190 | padding: 10px 0px; 191 | font-size: 1em; 192 | } 193 | button.single-btn { 194 | top: 0px; 195 | } 196 | .nav-pills>li, .nav-tabs>li { 197 | float: left; 198 | display: inline-block; 199 | zoom: 1; 200 | width: 50%; 201 | } 202 | .modal-open { 203 | padding-right: 0px !important; 204 | } 205 | } 206 | /*------------------------------------------------ 207 | Base Build Small Devices Common END 208 | ------------------------------------------------ */ 209 | 210 | /*------------------------------------------------ 211 | Base Build for Small devices Portrait START 212 | ------------------------------------------------ */ 213 | @media only screen and (min-width: 320px) and (max-width: 767px) and (orientation: portrait) { 214 | body { 215 | font-size: 14px; 216 | height: initial !important; 217 | } 218 | .mainbody-wrapper { 219 | min-height: 100vh; 220 | height: initial; 221 | position: relative; 222 | } 223 | .homepagebody-wrapper { 224 | min-height: 792px; 225 | height: 100vh; 226 | position: relative; 227 | } 228 | .modal-dialog { 229 | width: 100%; 230 | } 231 | } 232 | /*------------------------------------------------ 233 | Base Build for Small devices Portrait END 234 | ------------------------------------------------ */ 235 | 236 | 237 | /*------------------------------------------------ 238 | Base Build for Small devices Landscape START 239 | ------------------------------------------------ */ 240 | @media only screen and (min-width: 320px) and (max-width: 767px) and (orientation: landscape) { 241 | body { 242 | font-size: 14px; 243 | height: initial !important; 244 | } 245 | .mainbody-wrapper { 246 | min-height: 680px; 247 | height: initial; 248 | position: relative; 249 | } 250 | .homepagebody-wrapper { 251 | min-height: 650px; 252 | height: 100vh; 253 | position: relative; 254 | } 255 | } 256 | /*------------------------------------------------ 257 | Base Build for Small devices Landscape END 258 | ------------------------------------------------ */ 259 | 260 | 261 | /*------------------------------------------------ 262 | Common Large Tablet START 263 | ------------------------------------------------ */ 264 | @media only screen and (min-width: 768px) and (max-width: 1199px) { 265 | .homepagebody-wrapper { 266 | min-height: 694px; 267 | height: 100vh; 268 | position: relative; 269 | } 270 | .mainbody-wrapper { 271 | min-height: inherit; 272 | height: initial; 273 | position: relative; 274 | } 275 | .landingpagebody-wrapper { 276 | min-height: 100vh; 277 | height: 100vh; 278 | position: relative; 279 | } 280 | .commonbody-container { 281 | width: 100%; 282 | margin: 0 auto 25px auto; 283 | padding: 0 50px; 284 | } 285 | .mainHeaderContainer.container.hidden-xs { 286 | padding-left: 0px; 287 | padding-right: 0px; 288 | display: none; 289 | } 290 | 291 | /* --- Side Navigation Start --- */ 292 | .sidenav { 293 | height: 100%; 294 | width: 0; 295 | position: fixed; 296 | z-index: 100; 297 | top: 0; 298 | left: 0; 299 | background-color: #111; 300 | overflow-x: hidden; 301 | transition: .5s; 302 | padding-top: 15px; 303 | line-height: 30px; 304 | } 305 | .sidenav a { 306 | padding: 8px 8px 8px 32px; 307 | text-decoration: none; 308 | font-size: 1.3em; 309 | color: #fff; 310 | display: block; 311 | transition: .3s; 312 | } 313 | .sidenav a:hover { 314 | color: #f1f1f1; 315 | } 316 | .sidenav .closebtn { 317 | position: absolute; 318 | top: 10px; 319 | right: 25px; 320 | font-size: 2.57em; 321 | margin-left: 50px; 322 | color: #c0c0c0; 323 | } 324 | .navbar-collapse.collapse { 325 | display: none !important; 326 | } 327 | 328 | /* --- Mobile Header Start --- */ 329 | #mobile-header { 330 | transition: margin-left .5s; 331 | padding: 16px; 332 | height: 66px; 333 | width: 100%; 334 | border-bottom: 2px solid #E11F26; 335 | background-color: #fff; 336 | } 337 | .mobile-mockpass-logo { 338 | position: relative; 339 | top: -5px; 340 | height: 49px; 341 | padding: 0; 342 | width: 122px; 343 | background: url("../../resources/img/logo/mockpass-logo.png"); 344 | background-size: contain; 345 | background-repeat: no-repeat; 346 | margin: 0 auto; 347 | } 348 | .mNavigation_body p { 349 | padding: 18px 5px; 350 | margin: 0px; 351 | } 352 | .plusminus { 353 | float: right; 354 | position: relative; 355 | } 356 | .mNavigation_title { 357 | background-color: #111; 358 | margin: 0 0 1px 0; 359 | text-decoration: none; 360 | color: #fff; 361 | display: block; 362 | border-bottom: 1px solid #17202A; 363 | } 364 | .mNavigation_title.mDropdown { 365 | padding: 8px 8px 8px 32px; 366 | font-size: 1.3em; 367 | } 368 | .mNavigation_body { 369 | background: lightgray; 370 | padding: 0px 8px 0px 5px; 371 | text-decoration: none; 372 | font-size: .85em; 373 | color: #fff; 374 | display: block; 375 | transition: .5s; 376 | background-color: #17202A; 377 | } 378 | .mNavigation_body>a { 379 | border-bottom: 1px solid #212F3D; 380 | font-size: 1.1em; 381 | } 382 | /* --- Mobile Header END --- */ 383 | } 384 | /*------------------------------------------------ 385 | Common Large Tablet END 386 | ------------------------------------------------ */ 387 | 388 | 389 | /*------------------------------------------------ 390 | Large Tablet (SIZE: SM) START 391 | ------------------------------------------------ */ 392 | @media only screen and (min-width: 768px) and (max-width: 991px) { 393 | .inner-body-wrapper { 394 | min-height: 650px; 395 | padding-bottom: 75px; 396 | position: relative; 397 | } 398 | /* --- Main Footer --- */ 399 | .container-wrapper-footer { 400 | color: #fff; 401 | background-color: #e2dedf; 402 | text-align: center; 403 | padding: 10px 0 10px; 404 | margin: auto; 405 | position: absolute; 406 | } 407 | .footer-terms-condition { 408 | text-align: center !important; 409 | padding: 0 0 0 0 !important; 410 | width: 100% !important; 411 | font-size: 0.75em; 412 | color: #000; 413 | } 414 | .footer-terms-condition a { 415 | color: #000; 416 | } 417 | .footer-copyright { 418 | text-align: center !important; 419 | padding: 5px 25px 0 0; 420 | font-size: 0.75em; 421 | color: #000; 422 | font-weight: normal; 423 | } 424 | /* --- Main Footer End --- */ 425 | } 426 | /*------------------------------------------------ 427 | Large Tablet (SIZE: SM) END 428 | ------------------------------------------------ */ 429 | 430 | 431 | /*------------------------------------------------ 432 | Large Tablet (SIZE: MD) START 433 | ------------------------------------------------ */ 434 | @media only screen and (min-width: 992px) and (max-width: 1199px) { 435 | .inner-body-wrapper { 436 | padding-bottom: 55px; 437 | position: relative; 438 | } 439 | /* --- Main Footer --- */ 440 | .container-wrapper-footer { 441 | color: #fff; 442 | background-color: #e2dedf; 443 | text-align: center; 444 | padding: 20px 0 20px; 445 | margin: auto; 446 | position: absolute; 447 | } 448 | .footer-terms-condition { 449 | text-align: left; 450 | padding: 0; 451 | width: 100% !important; 452 | font-size: 0.75em; 453 | color: #000; 454 | } 455 | .footer-terms-condition a { 456 | color: #000; 457 | } 458 | .footer-copyright { 459 | text-align: right; 460 | padding: 0; 461 | font-size: 0.75em; 462 | color: #000; 463 | font-weight: normal; 464 | } 465 | /* --- Main Footer End --- */ 466 | } 467 | /*------------------------------------------------ 468 | Large Tablet (SIZE: MD) END 469 | ------------------------------------------------ */ 470 | 471 | 472 | /*------------------------------------------------ 473 | IPad Portrait 474 | ------------------------------------------------ */ 475 | @media only screen and (device-width: 768px) and (device-height: 1024px) and (orientation: portrait) { 476 | .inner-body-wrapper { 477 | min-height: 0px; 478 | padding-bottom: 75px; 479 | position: relative; 480 | } 481 | button.btn.btn-primary.btn-lg.eailogout-btn.logoutBtn { 482 | position: absolute; 483 | left: 87%; 484 | top: 29px; 485 | } 486 | /* --- Main Footer --- */ 487 | .container-wrapper-footer { 488 | color: #fff; 489 | background-color: #e2dedf; 490 | text-align: center; 491 | padding: 10px 0 10px; 492 | margin: auto; 493 | position: absolute; 494 | } 495 | .footer-terms-condition { 496 | text-align: center !important; 497 | padding: 0 0 0 0 !important; 498 | width: 100% !important; 499 | font-size: 0.75em; 500 | color: #000; 501 | } 502 | .footer-terms-condition a { 503 | color: #000; 504 | } 505 | .footer-copyright { 506 | text-align: center !important; 507 | padding: 5px 25px 0 0; 508 | font-size: 0.75em; 509 | color: #000; 510 | font-weight: normal; 511 | } 512 | /* --- Main Footer End --- */ 513 | } 514 | /*------------------------------------------------ 515 | IPad Portrait END 516 | ------------------------------------------------ */ 517 | 518 | /*------------------------------------------------ 519 | IPad Landscape 520 | ------------------------------------------------ */ 521 | @media only screen and (device-width: 768px) and (device-height: 1024px) and (orientation: landscape) { 522 | .inner-body-wrapper { 523 | min-height: 0px; 524 | padding-bottom: 55px; 525 | position: relative; 526 | } 527 | button.btn.btn-primary.btn-lg.logoutBtn { 528 | position: absolute; 529 | width: 229px; 530 | font-weight: bold; 531 | font-size: 1.2em; 532 | border-top-left-radius: 0px !important; 533 | border-top-right-radius: 0px !important; 534 | top: 1px; 535 | left: unset !important; 536 | } 537 | /* --- Main Footer --- */ 538 | .container-wrapper-footer { 539 | color: #fff; 540 | background-color: #e2dedf; 541 | text-align: center; 542 | padding: 20px 0 20px; 543 | margin: auto; 544 | position: absolute; 545 | } 546 | .footer-terms-condition { 547 | text-align: left; 548 | padding: 0; 549 | width: 100% !important; 550 | font-size: 0.75em; 551 | color: #000; 552 | } 553 | .footer-terms-condition a { 554 | color: #000; 555 | } 556 | .footer-copyright { 557 | text-align: right; 558 | padding: 0; 559 | font-size: 0.75em; 560 | color: #000; 561 | font-weight: normal; 562 | } 563 | /* --- Main Footer End --- */ 564 | } 565 | /*------------------------------------------------ 566 | IPad Landscape END 567 | ------------------------------------------------ */ -------------------------------------------------------------------------------- /public/mockpass/resources/css/style-common-small-media.css: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------ 2 | Base Build Small Devices Common START 3 | ------------------------------------------------ */ 4 | @media only screen and (min-width: 320px) and (max-width: 767px) { 5 | .trilinebreak { 6 | margin: 3em 0; 7 | line-height: 5em; 8 | } 9 | .noPageTitleContainer { 10 | position: relative; 11 | margin: 25px auto 25px auto; 12 | } 13 | .breadcrumb { 14 | color: #2a2d33; 15 | font-weight: normal; 16 | background-color: #fff; 17 | text-align: left; 18 | vertical-align: middle; 19 | list-style: none; 20 | padding: 5px 0 0 10px; 21 | height: 36px; 22 | margin-bottom: 0; 23 | font-size: .75em !important; 24 | } 25 | .form-horizontal .control-label { 26 | padding-top: 7px; 27 | padding-bottom: 7px; 28 | margin-bottom: 0; 29 | text-align: right; 30 | font-weight: normal; 31 | top: 0px; 32 | } 33 | .btn-wrapper { 34 | margin-top: 25px; 35 | margin-bottom: 25px; 36 | } 37 | .error-page-title { 38 | color: #E11F26; 39 | font-weight: bold; 40 | font-size: 1.2em; 41 | padding: 55px 0px 60px 0px; 42 | line-height: 22px; 43 | } 44 | .pageerror-img-icon { 45 | height: 95px; 46 | width: 95px; 47 | float: left; 48 | margin: 30px 10px 0px 0px; 49 | background-image: url(../../resources/img/message-icons-73f308d85125c45f0eb1579ae865de94.png); 50 | background-position: 0px -5px; 51 | background-size: 310px; 52 | } 53 | 54 | .announcement-container { 55 | text-align: left; 56 | padding: 0 20px; 57 | } 58 | 59 | /* Page Notification START */ 60 | .error-page-notification-wrapper { 61 | margin: 25px -9999rem 0px -9999rem; 62 | padding: 20px 9999rem; 63 | background: #fef7f7; 64 | color: #2a2d33; 65 | } 66 | .info-page-notification-wrapper { 67 | margin: 25px -9999rem 0px -9999rem; 68 | padding: 20px 9999rem; 69 | background: #ccf2f6; 70 | color: #2a2d33; 71 | } 72 | /* Page Notification END */ 73 | 74 | .primary-heading { 75 | font-size: 1.5em; 76 | color: #2a2d33; 77 | font-weight: bold; 78 | padding: 30px 0 30px 0px; 79 | } 80 | .confirmation-body-wrapper { 81 | height: auto; 82 | width: 100%; 83 | margin: 40px 0px 0px 0px; 84 | } 85 | .letter-content { 86 | line-height: 1.25 !important; 87 | margin: 0px 0px 50px 0px; 88 | } 89 | .error-mobile-token-notification-wrapper, .info-mobile-token-notification-wrapper { 90 | margin-top: 15px; 91 | } 92 | .error-content { 93 | padding-left: 0px; 94 | } 95 | .icon-img-success { 96 | height: 44px; 97 | width: 44px; 98 | background: url(../../resources/img/message-icons-73f308d85125c45f0eb1579ae865de94.png) no-repeat -95px; 99 | background-size: cover; 100 | } 101 | .success-page-notification-wrapper { 102 | margin: 20px -9999rem 0px -9999rem; 103 | } 104 | .success-page-notification-msg { 105 | padding: 0 0 0 10px; 106 | } 107 | } 108 | /*------------------------------------------------ 109 | Base Build Small Devices Common END 110 | ------------------------------------------------ */ 111 | 112 | 113 | 114 | /*------------------------------------------------ 115 | Common Large Tablet START 116 | ------------------------------------------------ */ 117 | @media only screen and (min-width: 768px) and (max-width: 1199px) { 118 | .noPageTitleContainer { 119 | position: relative; 120 | margin: 25px auto 50px auto; 121 | } 122 | .error-content { 123 | padding-left: 113px; 124 | } 125 | } 126 | /*------------------------------------------------ 127 | Common Large Tablet END 128 | ------------------------------------------------ */ 129 | 130 | 131 | /*------------------------------------------------ 132 | IPad Portrait 133 | ------------------------------------------------ */ 134 | @media only screen and (device-width: 768px) and (device-width: 1024px) and (orientation: portrait) { 135 | .alert-labelled-cell { 136 | padding: 10px 5px; 137 | display: table-cell; 138 | vertical-align: middle; 139 | line-height: 20px; 140 | } 141 | .error-content { 142 | padding-left: 113px; 143 | } 144 | } 145 | /*------------------------------------------------ 146 | IPad Portrait END 147 | ------------------------------------------------ */ 148 | 149 | /*------------------------------------------------ 150 | IPad Landscape 151 | ------------------------------------------------ */ 152 | @media only screen and (device-width: 768px) and (device-width: 1024px) and (orientation: landscape) { 153 | .error-content { 154 | padding-left: 113px; 155 | } 156 | } -------------------------------------------------------------------------------- /public/mockpass/resources/css/style-common.css: -------------------------------------------------------------------------------- 1 | /* Page Notification START */ 2 | .error-page-notification-wrapper { 3 | margin: 50px -9999rem 0px -9999rem; 4 | padding: 20px 9999rem; 5 | background: #fef7f7; 6 | color: #2a2d33; 7 | } 8 | .success-page-notification-wrapper { 9 | margin: 70px -9999rem 0px -9999rem; 10 | padding: 5px 9999rem; 11 | background: #eaf6f2; 12 | color: #2a2d33; 13 | } 14 | .info-page-notification-wrapper { 15 | margin: 50px -9999rem 0px -9999rem; 16 | padding: 20px 9999rem; 17 | background: #ccf2f6; 18 | color: #2a2d33; 19 | } 20 | 21 | .info-mobile-token-notification-wrapper { 22 | height: 100%; 23 | width: 100%; 24 | background-color: #ccf2f6; 25 | display: inline-block; 26 | } 27 | 28 | .error-mobile-token-notification-wrapper { 29 | height: 100%; 30 | width: 100%; 31 | background-color: #fef7f7; 32 | display: inline-block; 33 | } 34 | 35 | .iconImg-wrapper { 36 | margin: 1em 0; 37 | height: auto; 38 | width: 100%; 39 | position: relative; 40 | word-wrap: break-word; 41 | } 42 | .icon-img-info { 43 | height: 77px !important; 44 | width: 77px !important; 45 | position: absolute; 46 | float: left; 47 | top: 0; 48 | bottom: 0; 49 | left: 0; 50 | right: 0; 51 | margin: auto 0px auto 0px !important; 52 | background: url(../../resources/img/message-icons-73f308d85125c45f0eb1579ae865de94.png) no-repeat -90px; 53 | } 54 | 55 | .d-table-cell { 56 | display: table-cell; 57 | } 58 | .icon-img-success { 59 | height: 90px; 60 | width: 80px; 61 | background: url(../../resources/img/message-icons-73f308d85125c45f0eb1579ae865de94.png) no-repeat -174px; 62 | } 63 | .icon-img-info-mobile-token { 64 | height: 77px !important; 65 | width: 77px !important; 66 | margin: 10px 5px 10px 10px !important; 67 | display: inline-block !important; 68 | vertical-align: middle !important; 69 | line-height: auto !important; 70 | background: url(../../resources/img/message-icons-73f308d85125c45f0eb1579ae865de94.png) no-repeat -90px; 71 | } 72 | 73 | .icon-img-error { 74 | height: 77px !important; 75 | width: 77px !important; 76 | position: absolute; 77 | float: left; 78 | top: 0; 79 | bottom: 0; 80 | left: 0; 81 | right: 0; 82 | margin: auto 0px auto 0px !important; 83 | background: url(../../resources/img/message-icons-73f308d85125c45f0eb1579ae865de94.png) no-repeat -2px; 84 | } 85 | 86 | .icon-img-error-mobile-token { 87 | height: 77px !important; 88 | width: 77px !important; 89 | margin: 10px !important; 90 | display: inline-block !important; 91 | vertical-align: middle !important; 92 | line-height: auto !important; 93 | background: url(../../resources/img/message-icons-73f308d85125c45f0eb1579ae865de94.png) no-repeat -2px; 94 | } 95 | .error-page-notification-msg, .info-page-notification-msg { 96 | padding: 0px 0px 0px 100px; 97 | } 98 | .success-page-notification-msg { 99 | padding: 0 0 0 20px; 100 | vertical-align: middle; 101 | font-size: 1.3em; 102 | } 103 | .info-mobile-token-msg, .error-mobile-token-msg { 104 | display: inline-block; 105 | font-size: 0.875em; 106 | vertical-align: middle; 107 | width: calc(100% - 110px); 108 | padding: 10px 0px; 109 | } 110 | /* Page Notification END */ 111 | .apple, .google { 112 | display: inline-block; 113 | height: auto; 114 | width: 46.67%; 115 | max-height: 40px; 116 | max-width: 140px; 117 | } 118 | /* Progress Tracker */ 119 | .progress-tracker-container { 120 | display: inline-block; 121 | padding-right: 0; 122 | padding-left: 0; 123 | } 124 | ul.progress-tracker.progress-tracker--word.progress-tracker--word-center 125 | { 126 | padding-top: 10px; 127 | padding-bottom: 0px; 128 | height: 105px; 129 | } 130 | .progress-step { 131 | display: block; 132 | position: relative; 133 | -webkit-box-flex: 1; 134 | -ms-flex: 1 1 0; 135 | flex: 1 0 1%; 136 | margin-top: 10px; 137 | min-width: 10px !important; 138 | height: 103px !important; 139 | } 140 | .progress-tracker { 141 | display: -webkit-box; 142 | display: -ms-flexbox; 143 | display: flex; 144 | margin: 0px auto 50px auto !important; 145 | padding: 0; 146 | list-style: none; 147 | width: 324px !important; 148 | } 149 | .progress-tracker--word-center { 150 | padding-right: 50px !important; 151 | padding-left: 15px !important; 152 | float: left; 153 | } 154 | .progress-step.is-complete .progress-marker { 155 | background-color: #00a651 !important; 156 | border: 4px solid #868686 !important 157 | } 158 | .progress-step .progress-marker { 159 | color: #fff; 160 | background-color: #a1a1a1 !important; 161 | border: 4px solid #c0c0c0 !important; 162 | } 163 | .progress-step.is-active .progress-marker { 164 | background-color: #2ECC71 !important; 165 | font-weight: bold; 166 | } 167 | .progress-title.title-active { 168 | font-weight: bold !important; 169 | } 170 | .progress-marker { 171 | display: -webkit-box; 172 | display: -ms-flexbox; 173 | display: flex; 174 | -webkit-box-pack: center; 175 | -ms-flex-pack: center; 176 | justify-content: center; 177 | -webkit-box-align: center; 178 | -ms-flex-align: center; 179 | align-items: center; 180 | position: relative; 181 | z-index: unset !important; 182 | width: 48px !important; 183 | height: 48px !important; 184 | padding-bottom: 2px; 185 | color: #fff; 186 | font-weight: 400; 187 | border: 2px solid transparent; 188 | border-radius: 50%; 189 | -webkit-transition: background-color, border-color; 190 | transition: background-color, border-color; 191 | -webkit-transition-duration: .3s; 192 | transition-duration: .3s; 193 | top: -10px; 194 | left: -10px; 195 | } 196 | .progress-tracker--word { 197 | padding-right: 38.6666666667px; 198 | overflow: visible !important; 199 | } 200 | .progress-step::after { 201 | background-color: #b6b6b6 !important; 202 | } 203 | .progress-step.is-complete::after { 204 | background-color: #868686 !important; 205 | } 206 | .progress-text { 207 | text-align: center !important; 208 | padding: 0px !important; 209 | font-size: 0.85em; 210 | } 211 | /* Progress Tracker End */ 212 | 213 | /* Announcement Start */ 214 | .announcement-container { 215 | background: #f3f3f3; 216 | } 217 | .announcement-title { 218 | font-weight: bold; 219 | color: #E11F26; 220 | } 221 | .alert-labeled { 222 | padding: 0px; 223 | } 224 | .alert-labeled-row { 225 | display: table-row; 226 | padding: 0px; 227 | } 228 | .alert-labelled-cell { 229 | padding: 10px 0px; 230 | display: table-cell; 231 | vertical-align: middle; 232 | line-height: 20px; 233 | word-break: break-word; 234 | } 235 | .alert-labeled .close>* { 236 | padding: 10px; 237 | display: table-cell; 238 | vertical-align: middle; 239 | } 240 | .alert-label { 241 | width: 0px; 242 | height: 0px; 243 | font-size: 1.1em; 244 | padding: 0px; 245 | } 246 | .alert-info { 247 | position: relative; 248 | padding: 0px 0px; 249 | background: #f3f3f3; 250 | color: #000; 251 | border-color: #f3f3f3; 252 | margin-bottom: 0px 253 | } 254 | .alert-message { 255 | margin: 20px 0; 256 | padding: 20px 0px 20px 0px; 257 | border-bottom: 3px solid #eee; 258 | line-height: 30px; 259 | } 260 | .alert-message p:last-child { 261 | margin-bottom: 0; 262 | } 263 | .alert-message-default { 264 | /*background-color: #EEE; 265 | border-color: #B4B4B4;*/ 266 | 267 | } 268 | .alert-message-default .announcementdefault-title { 269 | color: #000; 270 | font-weight: bold; 271 | } 272 | /* Announcement Start End*/ 273 | 274 | .container { 275 | padding-right: 0; 276 | padding-left: 0; 277 | margin-right: auto; 278 | margin-left: auto; 279 | } 280 | .commonbody-container { 281 | width: 1170px; 282 | margin: 0px auto 110px auto; 283 | } 284 | .homepagebody-container { 285 | min-height: 0px; 286 | } 287 | .noPageTitleContainer { 288 | position: relative; 289 | margin: 50px auto 50px auto; 290 | } 291 | .bodyContentOnly { 292 | margin: 100px 0px 0px 0px; 293 | } 294 | .user-container { 295 | background-color: #e00b16; 296 | color: #fff; 297 | font-size: 1em; 298 | text-align: left; 299 | max-width: 700px; 300 | height: 50px; 301 | padding: 0 0 0 15px; 302 | } 303 | .confirmation-body-wrapper { 304 | height: 100%; 305 | width: 96%; 306 | margin: 70px auto 0px; 307 | } 308 | .breadcrumb { 309 | color: #2a2d33; 310 | font-weight: normal; 311 | background-color: #fff; 312 | text-align: left; 313 | vertical-align: middle; 314 | list-style: none; 315 | padding: 20px 0px 0px 0px; 316 | height: 18px; 317 | font-size: 0.85em !important; 318 | margin: 0px; 319 | } 320 | .breadcrumb>li a { 321 | color: #2a2d33; 322 | } 323 | .primary-heading { 324 | font-size: 1.5em; 325 | color: #2a2d33; 326 | font-weight: bold; 327 | padding: 50px 0; 328 | } 329 | .form-horizontal .control-label { 330 | padding-top: 0; 331 | margin-bottom: 0; 332 | text-align: right; 333 | font-weight: normal; 334 | top: 7px; 335 | } 336 | .linebreak:after { 337 | content: "\a0\a"; 338 | white-space: pre; 339 | } 340 | .doublelinebreak { 341 | margin: 1.25em 0; 342 | line-height: 5em; 343 | } 344 | .speciallinebreak { 345 | margin: 3.5em 0; 346 | line-height: 5em; 347 | } 348 | .trilinebreak { 349 | margin: 6em 0; 350 | line-height: 5em; 351 | } 352 | .mandatory-label { 353 | font-size: 1.1em; 354 | color: #E11F26; 355 | font-weight: bold; 356 | position: relative; 357 | } 358 | button.btn.btn-default.SearchButton { 359 | position: relative; 360 | top: -3px !important; 361 | } 362 | 363 | /* Info/Error Common START */ 364 | .row-error-wrapper { 365 | position: relative; 366 | margin: 0 -9999rem; 367 | padding: .25rem 9999rem; 368 | background: #fef7f7; 369 | color: #2a2d33; 370 | } 371 | .common-error-page-title { 372 | color: #000; 373 | font-weight: bold; 374 | font-size: 1em; 375 | padding: 27px 0 6px; 376 | line-height: 22px; 377 | } 378 | .common-error-page-title-description { 379 | color: #000; 380 | padding: 5px 0 0; 381 | font-weight: bold; 382 | font-size: 1em; 383 | } 384 | .error-field, input.form-control.error-field { 385 | border: 2px solid #E11F26; 386 | } 387 | .field-error-message { 388 | color: #E11F26; 389 | padding: 10px 0; 390 | font-size: 84%; 391 | } 392 | .mobile-error-field.field-error-message { 393 | color: #E11F26; 394 | padding: 10px 0; 395 | margin: 0 0 10px; 396 | position: relative; 397 | top: -10px; 398 | } 399 | .errorContainerNobreadcrumb, .infoContainerNobreadcrumb { 400 | position: relative; 401 | margin: 146px 0px 0px 0px; 402 | } 403 | .infoContainerNobreadcrumb { 404 | margin: 13px 0 0; 405 | } 406 | .row-info-wrapper { 407 | margin: 0 -9999rem; 408 | padding: 1rem 9999rem; 409 | background: #ccf2f6; 410 | color: #2a2d33; 411 | } 412 | .common-info-page-title { 413 | font-weight: bold; 414 | font-size: 1em; 415 | padding: 34px 0 6px; 416 | } 417 | /* Info/Error Common END */ 418 | 419 | /* Tooltip START */ 420 | .tooltip-icon { 421 | display: inline-block; 422 | position: relative; 423 | width: 16px; 424 | height: 16px; 425 | top: 2px; 426 | background-image: url("../../resources/img/utility-icon-abac0927fb10e94ee131988bcf12ed74.png"); 427 | background-position: -49px 0; 428 | background-repeat: no-repeat; 429 | cursor: pointer; 430 | } 431 | .icon-tooltip-token { 432 | position: relative; 433 | width: 81px; 434 | height: 80px; 435 | float: right; 436 | background: url("../../resources/img/smartphone-token-bace7209263063c84a9d1de4e355ce7c.png"); 437 | background-position: -510px 0; 438 | background-repeat: no-repeat; 439 | background-size: 600px; 440 | top: -50px; 441 | margin-bottom: 0; 442 | } 443 | .tooltipster-box>.tooltipster-content { 444 | padding: 20px 20px !important; 445 | } 446 | .tooltip-content { 447 | line-height: 1.25; 448 | font-size: 0.875em; 449 | text-align: left !important; 450 | } 451 | /* Tooltip END */ 452 | 453 | .letter-content { 454 | line-height: 1.25; 455 | } 456 | ul.token-links { 457 | line-height: 30px; 458 | padding: 0px 0px 0px 18px; 459 | } 460 | .updatedeviceotp.field-error-message { 461 | position: relative; 462 | left: -15px; 463 | } 464 | .errorpagecontainer { 465 | position: relative; 466 | margin: 50px 0px 0px 0px; 467 | } 468 | .error-page-wrapper { 469 | position: relative; 470 | margin: 0; 471 | padding: 0; 472 | color: #2a2d33; 473 | } 474 | .pageerror-img-icon { 475 | height: 250px; 476 | width: 102px; 477 | float: left; 478 | margin: 30px 10px 0px 0px; 479 | background-image: url(../../resources/img/message-icons-73f308d85125c45f0eb1579ae865de94.png); 480 | background-position: 0px -5px; 481 | background-size: 339px; 482 | background-repeat: no-repeat; 483 | } 484 | .error-page-title { 485 | color: #E11F26; 486 | font-weight: bold; 487 | font-size: 1.2em; 488 | padding: 35px 0 6px; 489 | line-height: 22px; 490 | } 491 | .error-page-desc { 492 | padding: 3px 0px 0px 0px; 493 | } 494 | .error-content { 495 | padding-left: 113px; 496 | } 497 | .btn-wrapper { 498 | margin-top: 50px; 499 | margin-bottom: 50px; 500 | } 501 | .blue-highlight-text { 502 | font-weight: bold; 503 | background-color: #D6EAF8; 504 | padding: 5px; 505 | color: #000; 506 | display: inline-block; 507 | } 508 | .page-note { 509 | color: #E11F26; 510 | } -------------------------------------------------------------------------------- /public/mockpass/resources/css/style-homepage-small-media.css: -------------------------------------------------------------------------------- 1 | /** Last Updated Date : 2018-12-28 11:00 AM */ 2 | 3 | /*------------------------------------------------ 4 | Base Build Small Devices Common START 5 | ------------------------------------------------ */ 6 | @media only screen and (min-width: 320px) and (max-width: 767px) { 7 | /* --- Login Modal Start --- */ 8 | .login-modal-content { 9 | background-color: #fff; 10 | position: relative; 11 | width: 100%; 12 | border-radius: 0; 13 | margin: 0px; 14 | } 15 | .innter-form { 16 | background-color: #fff; 17 | border-radius: 0; 18 | padding: 0px 20px; 19 | width: 100%; 20 | margin: 0px; 21 | } 22 | .login-form-body { 23 | background: transparent; 24 | padding: 0px; 25 | border-radius: 5px; 26 | width: 100%; 27 | margin: 0 auto; 28 | } 29 | .passwordless-field-lbl { 30 | text-align: center; 31 | display: block; 32 | margin: 20px; 33 | } 34 | .logout-link { 35 | padding-top: 5px; 36 | } 37 | button#loginModelbtn { 38 | background-color: #E11F26; 39 | border-color: #E11F26; 40 | width: 100%; 41 | height: 60px; 42 | font-size: 1.57em; 43 | font-weight: bold; 44 | border-radius: 4px; 45 | margin: 75px auto; 46 | } 47 | .modal-backdrop.in { 48 | opacity: 0; 49 | } 50 | .mobiletoken-form { 51 | top: 67px; 52 | position: relative; 53 | } 54 | li.mobiletokentab.active { 55 | position: relative; 56 | width: 50%; 57 | top: 0; 58 | } 59 | button.login-Btn { 60 | color: #fff; 61 | font-weight: bold; 62 | margin: 21px 0 33px; 63 | } 64 | button.cancel-Btn { 65 | margin: 21px 3px 33px; 66 | } 67 | button.eservicelogin-Btn { 68 | margin: 21px 0px 33px 69 | } 70 | /* --- Login Modal End --- */ 71 | 72 | /* --- Carousel Common START --- */ 73 | .carousel-container { 74 | margin-bottom: 45px; 75 | } 76 | .carousel-indicators { 77 | top: -34px; 78 | } 79 | .carousel { 80 | width: 100%; 81 | } 82 | .carousel-inner { 83 | position: relative; 84 | width: 100%; 85 | overflow: hidden; 86 | margin: 0px auto; 87 | padding: 0px; 88 | -webkit-transition: .5s ease-in-out left; 89 | -o-transition: .5s ease-in-out left; 90 | transition: .5s ease-in-out left; 91 | } 92 | a.regsp-icon { 93 | background: url(../../resources/img/carousel/small-device/mobile-register.png); 94 | background-repeat: no-repeat; 95 | background-position: center center; 96 | } 97 | a.resetpwd-icon { 98 | background: url(../../resources/img/carousel/small-device/mobile-reset-password-icon.png); 99 | background-repeat: no-repeat; 100 | background-position: center center; 101 | } 102 | a.myinfo-icon { 103 | background: url(../../resources/img/carousel/small-device/mobile-my-info-icon.png); 104 | background-repeat: no-repeat; 105 | background-position: center center; 106 | } 107 | a.updateAcctDetails-icon { 108 | background: url(../../resources/img/carousel/small-device/mobile-update-acct-icon.png); 109 | background-repeat: no-repeat; 110 | background-position: center center; 111 | } 112 | a.changePwd-icon { 113 | background: url(../../resources/img/carousel/small-device/mobile-change-pwd-icon.png); 114 | background-repeat: no-repeat; 115 | background-position: center center; 116 | } 117 | a.viewTransactionHistory-icon { 118 | background: url(../../resources/img/carousel/small-device/mobile-view-transaction-icon.png); 119 | background-repeat: no-repeat; 120 | background-position: center center; 121 | } 122 | a.setUp2Step2FA-icon { 123 | background: url(../../resources/img/carousel/small-device/mobile-setup-2fa-icon.png); 124 | background-repeat: no-repeat; 125 | background-position: center center; 126 | } 127 | a.checkapplication-icon { 128 | background: url(../../resources/img/carousel/small-device/mobile-chk-app-status-icon.png); 129 | background-repeat: no-repeat; 130 | background-position: center center; 131 | } 132 | a.retrieveid-icon { 133 | background: url(../../resources/img/carousel/small-device/mobile-retrieve-spid-icon.png); 134 | background-repeat: no-repeat; 135 | background-position: center center; 136 | } 137 | /* --- Carousel Common END --- */ 138 | } 139 | /*------------------------------------------------ 140 | Base Build Small Devices Common END 141 | ------------------------------------------------ */ 142 | 143 | 144 | /*------------------------------------------------ 145 | Base Build Small Devices Portrait START 146 | ------------------------------------------------ */ 147 | @media only screen and (min-width: 320px) and (max-width: 767px) and (orientation: portrait) { 148 | .sp-img-bg { 149 | background: url(../../resources/img/background/small-device/mobile-sp-bg.jpg) no-repeat bottom; 150 | -webkit-background-size: cover; 151 | -moz-background-size: cover; 152 | -o-background-size: cover; 153 | background-size: cover; 154 | height: calc(100vh - 69px); 155 | position: relative; 156 | min-height: 723px; 157 | } 158 | 159 | /* Carousel START*/ 160 | a.right.carousel-control { 161 | text-decoration: none !important; 162 | position: absolute; 163 | top: 66px; 164 | right: 0px; 165 | font-size: 24px; 166 | } 167 | a.left.carousel-control { 168 | text-decoration: none !important; 169 | position: absolute; 170 | top: 66px; 171 | left: -2px; 172 | font-size: 24px; 173 | } 174 | /* Carousel END*/ 175 | 176 | .loginbtn-container { 177 | padding-left: 10px; 178 | padding-right: 10px; 179 | margin-left: auto; 180 | margin-right: auto; 181 | } 182 | .btn-login { 183 | height: 47px !important; 184 | } 185 | .homepageLogin.modal-dialog { 186 | margin: 0px; 187 | left: 0px; 188 | right: 0px; 189 | padding: 0px; 190 | width: 100%; 191 | top: 148px; 192 | } 193 | .eserviceLoginForm.modal-dialog { 194 | margin: 0px; 195 | left:0px; 196 | right: 0px; 197 | padding: 0px; 198 | height: unset; 199 | width: 100%; 200 | } 201 | #myModalHorizontal { 202 | position:absolute; 203 | margin: 0px; 204 | left: 0px; 205 | right: 0px; 206 | padding: 0px; 207 | width: 100%; 208 | height: 100%; 209 | padding-left: 0px !important; 210 | padding-right: 0px !important; 211 | } 212 | } 213 | /*------------------------------------------------ 214 | Base Build Small Devices Portrait END 215 | ------------------------------------------------ */ 216 | 217 | /*------------------------------------------------ 218 | Base Build Small Devices Landscape START 219 | ------------------------------------------------ */ 220 | @media only screen and (min-width: 320px) and (max-width: 767px) and (orientation: landscape) { 221 | .sp-img-bg { 222 | background: url(../../resources/img/background/small-device/mobile-landscape-sp-bg.jpg) no-repeat bottom; 223 | -webkit-background-size: cover; 224 | -moz-background-size: cover; 225 | -o-background-size: cover; 226 | background-size: cover; 227 | min-height: calc(100% - 69px); 228 | height: unset; 229 | position: relative; 230 | } 231 | 232 | /* Carousel START*/ 233 | a.right.carousel-control { 234 | text-decoration: none !important; 235 | position: absolute; 236 | top: 66px; 237 | right: 0px; 238 | font-size: 24px; 239 | } 240 | a.left.carousel-control { 241 | text-decoration: none !important; 242 | position: absolute; 243 | top: 66px; 244 | left: -2px; 245 | font-size: 24px; 246 | } 247 | /* Carousel END*/ 248 | 249 | .homepageLogin.modal-dialog { 250 | right: 0; 251 | width: 100%; 252 | top: 148px; 253 | } 254 | .eserviceLoginForm.modal-dialog { 255 | left: 0; 256 | right: 0; 257 | width: 100%; 258 | margin: 0 0; 259 | } 260 | .loginbtn-container { 261 | padding-left: 10px; 262 | padding-right: 10px; 263 | margin-left: auto; 264 | margin-right: auto; 265 | } 266 | #myModalHorizontal { 267 | position:absolute; 268 | margin: 0px; 269 | left: 0px; 270 | right: 0px; 271 | padding: 0px !important; 272 | width: 100%; 273 | } 274 | } 275 | /*------------------------------------------------ 276 | Base Build Small Devices Landscape END 277 | ------------------------------------------------ */ 278 | 279 | 280 | 281 | /*------------------------------------------------ 282 | Common Large Tablet START 283 | ------------------------------------------------ */ 284 | @media only screen and (min-width: 768px) and (max-width: 1199px) { 285 | .homepageLogin.modal-dialog { 286 | right: 0px; 287 | margin: 0 2%; 288 | } 289 | .eserviceLoginForm.modal-dialog { 290 | top: 10px; 291 | right: 0px; 292 | margin: 0 1%; 293 | } 294 | .loginbtn-container { 295 | padding-left: 10px; 296 | padding-right: 10px; 297 | margin-left: auto; 298 | margin-right: auto; 299 | } 300 | button#loginModelbtn { 301 | background-color: #E11F26; 302 | border-color: #E11F26; 303 | width: 219px; 304 | height: 46px; 305 | font-size: 1.57em; 306 | font-weight: bold; 307 | border-radius: 4px; 308 | left: -14px; 309 | position: relative; 310 | } 311 | 312 | /* --- Carousel Common START --- */ 313 | .carousel { 314 | width: 100%; 315 | } 316 | .carousel-inner { 317 | position: relative; 318 | width: 100%; 319 | overflow: hidden; 320 | margin: 0px auto; 321 | padding: 0px; 322 | -webkit-transition: .5s ease-in-out left; 323 | -o-transition: .5s ease-in-out left; 324 | transition: .5s ease-in-out left; 325 | } 326 | a.regsp-icon { 327 | background: url(../../resources/img/carousel/medium-device/ipad-register-icon.png); 328 | background-repeat: no-repeat; 329 | background-position: center center; 330 | } 331 | a.setup2fa-icon { 332 | background: url(../../resources/img/carousel/medium-device/ipad-setup-2fa-icon.png); 333 | background-repeat: no-repeat; 334 | background-position: center center; 335 | } 336 | a.resetpwd-icon { 337 | background: url(../../resources/img/carousel/medium-device/ipad-reset-password-icon.png); 338 | background-repeat: no-repeat; 339 | background-position: center center; 340 | } 341 | a.myinfo-icon { 342 | background: url(../../resources/img/carousel/medium-device/ipad-my-info-icon.png); 343 | background-repeat: no-repeat; 344 | background-position: center center; 345 | } 346 | a.updateAcctDetails-icon { 347 | background: url(../../resources/img/carousel/medium-device/ipad-update-acct-icon.png); 348 | background-repeat: no-repeat; 349 | background-position: center center; 350 | } 351 | a.changePwd-icon { 352 | background: url(../../resources/img/carousel/medium-device/ipad-change-pwd-icon.png); 353 | background-repeat: no-repeat; 354 | background-position: center center; 355 | } 356 | a.viewTransactionHistory-icon { 357 | background: url(../../resources/img/carousel/medium-device/ipad-view-transaction-icon.png); 358 | background-repeat: no-repeat; 359 | background-position: center center; 360 | } 361 | a.setUp2Step2FA-icon { 362 | background: url(../../resources/img/carousel/medium-device/ipad-setup-2fa-icon.png); 363 | background-repeat: no-repeat; 364 | background-position: center center; 365 | } 366 | a.checkapplication-icon { 367 | background: url(../../resources/img/carousel/medium-device/ipad-app-status.png); 368 | background-repeat: no-repeat; 369 | background-position: center center; 370 | } 371 | a.retrieveid-icon { 372 | background: url(../../resources/img/carousel/medium-device/ipad-retrieve-spid-icon.png); 373 | background-repeat: no-repeat; 374 | background-position: center center; 375 | } 376 | /* --- Carousel Common END --- */ 377 | } 378 | /*------------------------------------------------ 379 | Common Large Tablet END 380 | ------------------------------------------------ */ 381 | 382 | 383 | /*------------------------------------------------ 384 | Large Tablet (Portrait - SM) START 385 | ------------------------------------------------ */ 386 | @media only screen and (min-width: 768px) and (max-width: 991px) and (orientation: portrait) { 387 | .sp-img-bg { 388 | background: url(../../resources/img/background/medium-device/ipad-bg.jpg) no-repeat bottom; 389 | -webkit-background-size: cover; 390 | -moz-background-size: cover; 391 | -o-background-size: cover; 392 | background-repeat: no-repeat; 393 | background-size: cover; 394 | height: unset; 395 | min-height: calc(100% - 75px); 396 | position: relative; 397 | } 398 | 399 | /* Carousel START*/ 400 | .carousel-container { 401 | margin-bottom: 75px; 402 | } 403 | a.right.carousel-control { 404 | text-decoration: none !important; 405 | position: absolute; 406 | top: 60px; 407 | right: 15px; 408 | font-size: 24px; 409 | } 410 | a.left.carousel-control { 411 | text-decoration: none !important; 412 | position: absolute; 413 | top: 60px; 414 | left: 15px; 415 | font-size: 24px; 416 | } 417 | /* Carousel END*/ 418 | } 419 | 420 | /*------------------------------------------------ 421 | Large Tablet (Portrait - SM) END 422 | ------------------------------------------------ */ 423 | 424 | 425 | /*------------------------------------------------ 426 | Large Tablet (Landscape - SM) START 427 | ------------------------------------------------ */ 428 | @media only screen and (min-width: 768px) and (max-width: 991px) and (orientation: landscape) { 429 | .sp-img-bg { 430 | background: url(../../resources/img/background/medium-device/ipad-landscape-sp-bg.jpg) no-repeat bottom; 431 | -webkit-background-size: cover; 432 | -moz-background-size: cover; 433 | -o-background-size: cover; 434 | background-repeat: no-repeat; 435 | background-size: cover; 436 | height: unset; 437 | min-height: calc(100% - 75px); 438 | position: relative; 439 | } 440 | 441 | /* Carousel START*/ 442 | .carousel-container { 443 | margin-bottom: 75px; 444 | } 445 | a.right.carousel-control { 446 | text-decoration: none !important; 447 | position: absolute; 448 | top: 60px; 449 | right: 15px; 450 | font-size: 24px; 451 | } 452 | a.left.carousel-control { 453 | text-decoration: none !important; 454 | position: absolute; 455 | top: 60px; 456 | left: 15px; 457 | font-size: 24px; 458 | } 459 | /* Carousel END*/ 460 | } 461 | 462 | /*------------------------------------------------ 463 | Large Tablet (Portrait - SM) END 464 | ------------------------------------------------ */ 465 | 466 | 467 | /*------------------------------------------------ 468 | Large Tablet (Landscape - MD) START 469 | ------------------------------------------------ */ 470 | @media only screen and (min-width: 992px) and (max-width: 1199px) { 471 | .sp-img-bg { 472 | background: url(../../resources/img/background/medium-device/ipad-landscape-sp-bg.jpg) no-repeat bottom; 473 | -webkit-background-size: cover; 474 | -moz-background-size: cover; 475 | -o-background-size: cover; 476 | background-size: cover; 477 | min-height: calc(100% - 55px); 478 | position: relative; 479 | } 480 | 481 | /* Carousel START*/ 482 | .carousel-container { 483 | margin-bottom: 55px; 484 | } 485 | a.right.carousel-control { 486 | text-decoration: none !important; 487 | position: absolute; 488 | top: 60px; 489 | right: 0px; 490 | font-size: 24px; 491 | } 492 | a.left.carousel-control { 493 | text-decoration: none !important; 494 | position: absolute; 495 | top: 60px; 496 | left: 0px; 497 | font-size: 24px; 498 | } 499 | /* Carousel END*/ 500 | } 501 | 502 | /*------------------------------------------------ 503 | Large Tablet (Landscape) END 504 | ------------------------------------------------ */ 505 | 506 | 507 | /*------------------------------------------------ 508 | IPad Portrait START 509 | ------------------------------------------------ */ 510 | @media only screen and (device-width: 768px) and (device-height: 1024px) and (orientation: portrait) { 511 | .sp-img-bg { 512 | background: url(../../resources/img/background/medium-device/ipad-bg.jpg) no-repeat bottom; 513 | -webkit-background-size: cover; 514 | -moz-background-size: cover; 515 | -o-background-size: cover; 516 | background-repeat: no-repeat; 517 | background-size: cover; 518 | min-height: calc(100vh - 75px); 519 | position: relative; 520 | } 521 | .login-modal-content { 522 | background-color: transparent; 523 | width: 400px; 524 | } 525 | /* Carousel START*/ 526 | .carousel-container { 527 | margin-bottom: 75px; 528 | } 529 | a.right.carousel-control { 530 | text-decoration: none !important; 531 | position: absolute; 532 | top: 60px; 533 | right: 15px; 534 | font-size: 24px; 535 | } 536 | a.left.carousel-control { 537 | text-decoration: none !important; 538 | position: absolute; 539 | top: 60px; 540 | left: 15px; 541 | font-size: 24px; 542 | } 543 | /* Carousel END*/ 544 | } 545 | /*------------------------------------------------ 546 | IPad Portrait END 547 | ------------------------------------------------ */ 548 | 549 | /*------------------------------------------------ 550 | IPad Landscape START 551 | ------------------------------------------------ */ 552 | @media only screen and (device-width: 768px) and (device-height: 1024px) and (orientation: landscape) { 553 | 554 | .sp-img-bg { 555 | background: url(../../resources/img/background/medium-device/ipad-landscape-sp-bg.jpg) no-repeat bottom; 556 | -webkit-background-size: cover; 557 | -moz-background-size: cover; 558 | -o-background-size: cover; 559 | background-size: cover; 560 | min-height: calc(100vh - 55px); 561 | position: relative; 562 | } 563 | 564 | /* Carousel START*/ 565 | .carousel-container { 566 | margin-bottom: 55px; 567 | } 568 | a.right.carousel-control { 569 | text-decoration: none !important; 570 | position: absolute; 571 | top: 60px; 572 | right: 0px; 573 | font-size: 24px; 574 | } 575 | a.left.carousel-control { 576 | text-decoration: none !important; 577 | position: absolute; 578 | top: 60px; 579 | left: 0px; 580 | font-size: 24px; 581 | } 582 | /* Carousel END*/ 583 | 584 | 585 | } 586 | /*------------------------------------------------ 587 | IPad Landscape END 588 | ------------------------------------------------ */ -------------------------------------------------------------------------------- /public/mockpass/resources/css/style-homepage.css: -------------------------------------------------------------------------------- 1 | /** Last Updated Date : 2018-12-28 11:00 AM */ 2 | 3 | body { 4 | font-family: "Open Sans", sans-serif; 5 | color: #2a2d33; 6 | background: #fff; 7 | overflow-x: hidden; 8 | } 9 | 10 | .dropdown-menu{ 11 | height: auto; 12 | max-height: 300px; 13 | overflow-x: hidden; 14 | } 15 | 16 | .sp-img-bg { 17 | background: url(../../resources/img/background/large-device/sp_bg.jpg) no-repeat bottom; 18 | -webkit-background-size: cover; 19 | -moz-background-size: cover; 20 | -o-background-size: cover; 21 | background-size: cover; 22 | height: calc(100vh - 55px); 23 | position: relative; 24 | min-height: 675px; 25 | } 26 | 27 | /* Login Modal START */ 28 | #myModalHorizontal { 29 | position:absolute; 30 | margin: 0px; 31 | left: 0px; 32 | right: 0px; 33 | padding: 0px !important; 34 | width: 100%; 35 | height: 100%; 36 | } 37 | 38 | #cr_fonts_frame { 39 | position: absolute; 40 | } 41 | 42 | .homepageLogin.modal-dialog { 43 | position: relative; 44 | top: 168px; 45 | margin: 0px; 46 | right: 3%; 47 | float: right; 48 | } 49 | .eserviceLoginForm.modal-dialog { 50 | position: relative; 51 | margin: 0; 52 | } 53 | .eserviceLoginForm.st-login.modal-dialog .login-form-body { 54 | margin: 85px 0 15px; 55 | } 56 | .eserviceLoginForm.st-login.modal-dialog .modal-content{ 57 | border-radius: 0; 58 | } 59 | .singpass-mobile-tab-note { 60 | text-align: center; 61 | font-size: 0.875em; 62 | display: block; 63 | margin: 10px 10px 10px 10px; 64 | } 65 | .passwordless-field-lbl { 66 | text-align: center; 67 | font-size: 0.875em; 68 | display: block; 69 | } 70 | /* Login Modal END */ 71 | 72 | /* Login Modal Tooltip START */ 73 | .sp-mobile-tooltip { 74 | line-height: 13.5px; 75 | transform-origin: calc(100% + 20px) center; 76 | font-family: 'Open Sans', sans-serif; 77 | position: absolute; 78 | font-size: 12px; 79 | max-width: 155px; 80 | padding: 10px; 81 | border-radius: 5px; 82 | background: #ED1C2E; 83 | color: white; 84 | right: 92px; 85 | top: 10px; 86 | z-index: 10; 87 | cursor: pointer; 88 | } 89 | .sp-mobile-tooltip::after { 90 | content: ''; 91 | display: block; 92 | width: 0; 93 | height: 0; 94 | border-top: 5px solid transparent; 95 | border-bottom: 5px solid transparent; 96 | position: absolute; 97 | border-left: 20px solid #ed1c2e; 98 | top: 50%; 99 | right: -20px; 100 | transform: translateY(-50%); 101 | } 102 | /* Login Modal Tooltip END */ 103 | 104 | /* Carousel START */ 105 | .carousel-container { 106 | position: absolute; 107 | width: 100%; 108 | bottom: 0; 109 | margin-bottom: 55px; 110 | } 111 | .carousel-indicators { 112 | top: -15px; 113 | position: relative; 114 | margin: 0 0 0 0; 115 | left: 0; 116 | width: 100%; 117 | padding: 0px; 118 | } 119 | .carousel { 120 | position: relative; 121 | width: 80%; 122 | height : 100%; 123 | margin: 0 auto; 124 | left: 0px; 125 | top: 0px; 126 | } 127 | a.right.carousel-control { 128 | text-decoration: none !important; 129 | position: absolute; 130 | top: 60px; 131 | right: -20px; 132 | font-size: 40px; 133 | opacity: 1; 134 | } 135 | a.left.carousel-control { 136 | text-decoration: none !important; 137 | position: absolute; 138 | top: 60px; 139 | left: -20px; 140 | font-size: 40px; 141 | opacity: 1; 142 | } 143 | .carousel-control.left, 144 | .carousel-control.right { 145 | background-image: none !important; 146 | } 147 | .carousel-control { 148 | left: -12px; 149 | height: 40px; 150 | width: 40px; 151 | font-size: 3em; 152 | background: none; 153 | border: none; 154 | border-radius: 23px 23px 23px 23px; 155 | margin-top: 0px; 156 | opacity: 1; 157 | } 158 | a.regsp-icon, a.setup2fa-icon, a.resetpwd-icon, a.myinfo-icon, a.updateAcctDetails-icon, 159 | a.changePwd-icon, a.viewTransactionHistory-icon, a.setUp2Step2FA-icon, 160 | a.checkapplication-icon, a.retrieveid-icon { 161 | width: auto; 162 | height: 200px; 163 | margin: 0 auto 0 auto; 164 | } 165 | a.regsp-icon { 166 | background: url(../../resources/img/carousel/large-device/register-icon.png); 167 | background-repeat: no-repeat; 168 | background-position: center center; 169 | } 170 | a.setup2fa-icon { 171 | background: url(../../resources/img/carousel/large-device/how-to-setup-2fa-icon.png); 172 | background-repeat: no-repeat; 173 | background-position: center center; 174 | } 175 | a.resetpwd-icon { 176 | background: url(../../resources/img/carousel/large-device/reset-password-icon.png); 177 | background-repeat: no-repeat; 178 | background-position: center center; 179 | } 180 | a.myinfo-icon { 181 | background: url(../../resources/img/carousel/large-device/my-info-icon.png); 182 | background-repeat: no-repeat; 183 | background-position: center center; 184 | } 185 | a.updateAcctDetails-icon { 186 | background: url(../../resources/img/carousel/large-device/update-acct-icon.png); 187 | background-repeat: no-repeat; 188 | background-position: center center; 189 | } 190 | a.changePwd-icon { 191 | background: url(../../resources/img/carousel/large-device/change-pwd-icon.png); 192 | background-repeat: no-repeat; 193 | background-position: center center; 194 | } 195 | a.viewTransactionHistory-icon { 196 | background: url(../../resources/img/carousel/large-device/view-transaction-icon.png); 197 | background-repeat: no-repeat; 198 | background-position: center center; 199 | } 200 | a.setUp2Step2FA-icon { 201 | background: 202 | url(../../resources/img/carousel/large-device/setup-2fa-icon.png); 203 | background-repeat: no-repeat; 204 | background-position: center center; 205 | } 206 | a.checkapplication-icon { 207 | background: url(../../resources/img/carousel/large-device/chk-app-status-icon.png); 208 | background-repeat: no-repeat; 209 | background-position: center center; 210 | } 211 | a.retrieveid-icon { 212 | background: url(../../resources/img/carousel/large-device/retrieve-spid-icon.png); 213 | background-repeat: no-repeat; 214 | background-position: center center; 215 | } 216 | /* Carousel END*/ 217 | 218 | .login-captcha-refresh { 219 | position: relative; 220 | top: -20px; 221 | margin-left: 5px; 222 | } 223 | 224 | .modal-dialog { 225 | width: 396px; 226 | } 227 | 228 | /*------ LOGIN STYLE ------*/ 229 | .modal-backdrop.in { 230 | filter: alpha(opacity=50); 231 | opacity: .5; 232 | } 233 | 234 | .modal-body .form-horizontal .col-sm-10, 235 | .modal-body .form-horizontal .col-sm-2 { 236 | width: 100%; 237 | } 238 | 239 | .modal-body .form-horizontal .control-label { 240 | text-align: left; 241 | } 242 | 243 | .modal-body .form-horizontal .col-sm-offset-2 { 244 | margin-left: 15px; 245 | } 246 | 247 | .login-modal-content { 248 | background-color: transparent; 249 | overflow: hidden; 250 | } 251 | 252 | .modal-dialog { 253 | box-shadow: 0 0 10px 1px black; 254 | } 255 | /*custimize */ 256 | #myModalHorizontal .hidden-label, .eserviceLoginForm .hidden-label { 257 | clip: rect(0,0,0,0); 258 | height: 1px; 259 | width: 1px; 260 | overflow: hidden; 261 | padding: 0; 262 | position: absolute; 263 | margin: -1px; 264 | } 265 | 266 | /*Form start */ 267 | .login-note { 268 | font-size: 0.9375em; 269 | color: #696671; 270 | padding-bottom: 13px; 271 | padding-top: 20px; 272 | position: relative; 273 | } 274 | #sectionB .login-note { 275 | padding-top: 10px; 276 | } 277 | .login-note p.note { 278 | font-weight: 600; 279 | font-size: 0.9em; 280 | white-space: nowrap; 281 | } 282 | .login-note p.note a { 283 | font-weight: 600; 284 | } 285 | .login-note p.userguide { 286 | font-size: 0.9em; 287 | padding-top: 15px; 288 | } 289 | /*Form start */ 290 | .qr-refresh-btn, .qr-get-new-qr-btn { 291 | font-size: 1.3em !important; 292 | font-weight: bold; 293 | height: auto; 294 | margin: 24px 0 39px !important; 295 | display: none; 296 | } 297 | .qr-image { 298 | position: relative; 299 | height: 195px; 300 | margin-top: -41px; 301 | z-index: 2; 302 | perspective: 400px; 303 | perspective-origin: center center; 304 | transform-style: preserve-3d; 305 | } 306 | .qr-logo-overlay { 307 | display: block; 308 | position: absolute; 309 | left: 50%; 310 | top: 50%; 311 | width: 65px; 312 | height: 65px; 313 | -webkit-transform: translate(-50%, 50%); 314 | -moz-transform: translate(-50%, 50%); 315 | -ms-transform: translate(-50%, 50%); 316 | transform: translate(-50%, -50%); 317 | } 318 | .qr-image #qrImage[src=""] + .qr-logo-overlay { 319 | display: none; 320 | } 321 | 322 | .qr-image::after { 323 | content: ""; 324 | pointer-events: none; 325 | display: block; 326 | width: 90%; 327 | height: 100%; 328 | background-color: rgba(255,255,255, 0.0); 329 | position: absolute; 330 | top: 0; 331 | left: 0; 332 | transition: background-color 0.2s ease-out; 333 | } 334 | .qr-image #qrcodelink { 335 | display: block; 336 | position: absolute; 337 | left: 50%; 338 | -webkit-transform: translateX(-50%); 339 | -moz-transform: translateX(-50%); 340 | -ms-transform: translateX(-50%); 341 | transform: translateX(-50%); 342 | width: 195px; 343 | height: 195px; 344 | -webkit-transition: all 750ms cubic-bezier(0.860, 0.000, 0.070, 1.000); 345 | -moz-transition: all 750ms cubic-bezier(0.860, 0.000, 0.070, 1.000); 346 | -o-transition: all 750ms cubic-bezier(0.860, 0.000, 0.070, 1.000); 347 | transition: all 750ms cubic-bezier(0.860, 0.000, 0.070, 1.000); 348 | } 349 | .qr-image #qrcodelink.flip { 350 | -webkit-transform: translateX(-50%) scale(.8); 351 | -moz-transform: translateX(-50%) scale(.8);; 352 | -ms-transform: translateX(-50%) scale(.8);; 353 | transform: translateX(-50%) scale(.8); 354 | opacity: 0; 355 | } 356 | .qr-image img#qrImage{ 357 | display: block; 358 | width: 195px; 359 | height: 195px; 360 | -webkit-image-rendering: pixelated; 361 | image-rendering: pixelated; 362 | cursor: pointer; 363 | transform: scale(1); 364 | opacity: 1; 365 | -webkit-user-select: none; 366 | -moz-user-select: none; 367 | -ms-user-select: none; 368 | user-select: none; 369 | } 370 | .qr-image .qr-image__success { 371 | display: block; 372 | position: absolute; 373 | opacity: 0; 374 | left: 100%; 375 | -webkit-transform: translateX(-50%); 376 | -moz-transform: translateX(-50%); 377 | -ms-transform: translateX(-50%); 378 | transform: translateX(-50%); 379 | width: auto; 380 | height: 185px; 381 | margin-top: 5px; 382 | -webkit-transition: all 750ms cubic-bezier(0.860, 0.000, 0.070, 1.000); 383 | -moz-transition: all 750ms cubic-bezier(0.860, 0.000, 0.070, 1.000); 384 | -o-transition: all 750ms cubic-bezier(0.860, 0.000, 0.070, 1.000); 385 | transition: all 750ms cubic-bezier(0.860, 0.000, 0.070, 1.000); 386 | } 387 | .qr-label__wrapper { 388 | position:relative; 389 | height: 54px; 390 | } 391 | .qr-label { 392 | font-size: 1.1em; 393 | font-weight: bold; 394 | color: #696671; 395 | padding-top: 8px; 396 | position: absolute; 397 | left: 50%; 398 | -webkit-transform: translateX(-50%); 399 | -moz-transform: translateX(-50%); 400 | -ms-transform: translateX(-50%); 401 | transform: translateX(-50%); 402 | -moz-user-select: none; 403 | -ms-user-select: none; 404 | user-select: none; 405 | width: 100%; 406 | } 407 | .qr-label--main { 408 | -webkit-transition: all 750ms cubic-bezier(0.860, 0.000, 0.070, 1.000) 50ms; 409 | -moz-transition: all 750ms cubic-bezier(0.860, 0.000, 0.070, 1.000) 50ms; 410 | -o-transition: all 750ms cubic-bezier(0.860, 0.000, 0.070, 1.000) 50ms; 411 | transition: all 750ms cubic-bezier(0.860, 0.000, 0.070, 1.000) 50ms; 412 | } 413 | .qr-label--success { 414 | opacity: 0; 415 | position: absolute; 416 | left: 100%; 417 | -webkit-transform: translateX(-50%); 418 | -moz-transform: translateX(-50%); 419 | -ms-transform: translateX(-50%); 420 | transform: translateX(-50%); 421 | -webkit-transition: all 750ms cubic-bezier(0.860, 0.000, 0.070, 1.000) 50ms; 422 | -moz-transition: all 750ms cubic-bezier(0.860, 0.000, 0.070, 1.000) 50ms; 423 | -o-transition: all 750ms cubic-bezier(0.860, 0.000, 0.070, 1.000) 50ms; 424 | transition: all 750ms cubic-bezier(0.860, 0.000, 0.070, 1.000) 50ms; 425 | } 426 | .qr-label.qr-label--small { 427 | font-size: 0.9375em; 428 | font-weight: normal; 429 | } 430 | .qr-main { 431 | display: block; 432 | height: 301px; 433 | } 434 | .qr-unavailable, .qr-suspended-account, .qr-locked-account { 435 | margin-top: -20px; 436 | height: 264px; 437 | display: none; 438 | } 439 | .qr-unavailable img{ 440 | display: block; 441 | margin: 16px auto 9px; 442 | width: 60px; 443 | height: 60px; 444 | } 445 | .qr__wrapper .qr-image .qr-error { 446 | -moz-user-select: none; 447 | -webkit-user-select: none; 448 | -ms-user-select: none; 449 | user-select: none; 450 | width: 100%; 451 | color: #696671; 452 | font-weight: bold; 453 | font-size: 1.1em; 454 | position: absolute; 455 | top: 50%; 456 | -webkit-transform: translateY(-50%) scale(1.5); 457 | -moz-transform: translateY(-50%) scale(1.5); 458 | -ms-transform: translateY(-50%) scale(1.5); 459 | transform: translateY(-50%) scale(1.5); 460 | z-index: 1; 461 | opacity: 0; 462 | display: block; 463 | padding: 0; 464 | pointer-events: none; 465 | -webkit-transition: all 250ms cubic-bezier(0.175, 0.885, 0.320, 1) 50ms; 466 | -webkit-transition: all 250ms cubic-bezier(0.175, 0.885, 0.320, 1.275) 50ms; 467 | -moz-transition: all 250ms cubic-bezier(0.175, 0.885, 0.320, 1.275) 50ms; 468 | -o-transition: all 250ms cubic-bezier(0.175, 0.885, 0.320, 1.275) 50ms; 469 | transition: all 250ms cubic-bezier(0.175, 0.885, 0.320, 1.275) 50ms; 470 | } 471 | .qr__wrapper .qr-image .qr-error.qr-error--suspended, .qr__wrapper .qr-image .qr-error.qr-error--locked { 472 | padding: 40px 22px 0; 473 | } 474 | .qr__wrapper .qr-image .qr-error span { 475 | font-size: 0.9375em; 476 | font-weight: normal; 477 | display: block; 478 | padding-top: 8px; 479 | } 480 | .qr__wrapper.has-scanned .qr-image #qrcodelink, .qr__wrapper.has-scanned .qr-label--main { 481 | opacity: 0; 482 | left: 0%; 483 | } 484 | .qr__wrapper.has-scanned .qr-image .qr-image__success { 485 | opacity: 1; 486 | left: 50%; 487 | } 488 | .qr__wrapper.has-scanned .qr-label--success { 489 | left: 50%; 490 | opacity: 1; 491 | } 492 | 493 | .qr__wrapper.is-expired .qr-image::after { 494 | pointer-events: all; 495 | background-color: rgba(255,255,255, 0.95); 496 | } 497 | .qr__wrapper.cant-gen .qr-image::after, .qr__wrapper.is-suspended .qr-image::after, .qr__wrapper.is-locked .qr-image::after { 498 | pointer-events: all; 499 | background-color: rgba(255,255,255, 1); 500 | } 501 | .qr__wrapper.is-expired .qr-image .qr-error:not(.qr-error--cant-gen):not(.qr-error--suspended):not(.qr-error--locked), 502 | .qr__wrapper.cant-gen .qr-image .qr-error:not(.qr-error--expired):not(.qr-error--suspended):not(.qr-error--locked), 503 | .qr__wrapper.is-suspended .qr-image .qr-error:not(.qr-error--expired):not(.qr-error--cant-gen):not(.qr-error--locked), 504 | .qr__wrapper.is-locked .qr-image .qr-error:not(.qr-error--expired):not(.qr-error--cant-gen):not(.qr-error--suspended) { 505 | opacity: 1; 506 | -webkit-transform: translateY(-50%) scale(1); 507 | -moz-transform: translateY(-50%) scale(1); 508 | -ms-transform: translateY(-50%) scale(1); 509 | transform: translateY(-50%) scale(1); 510 | } 511 | 512 | 513 | .qr__wrapper.is-expired .qr-label__wrapper, .qr__wrapper.cant-gen .qr-label__wrapper, 514 | .qr__wrapper.is-suspended .qr-label__wrapper, .qr__wrapper.is-locked .qr-label__wrapper { 515 | display: none; 516 | } 517 | .qr__wrapper.is-expired .qr-refresh-btn:not(.qr-get-new-qr-btn) { 518 | display: block; 519 | } 520 | .qr__wrapper.cant-gen .qr-get-new-qr-btn:not(.qr-refresh-btn) { 521 | display: block; 522 | } 523 | 524 | .qr__wrapper.is-unavailable .qr-unavailable { 525 | display: block; 526 | } 527 | .qr__wrapper.is-unavailable .qr-label { 528 | position: relative; 529 | } 530 | .qr__wrapper.is-unavailable .qr-main { 531 | display: none; 532 | } 533 | 534 | .login__footer { 535 | position: relative; 536 | height: 100px; 537 | } 538 | 539 | .login__footer::before { 540 | content: ""; 541 | display: block; 542 | background-color: #E2DEDF; 543 | width: calc(100% + 96px); 544 | height: 100%; 545 | position: absolute; 546 | left: -48px; 547 | top: 0; 548 | } 549 | .login__footer a { 550 | font-weight: normal; 551 | } 552 | .login-label { 553 | font-size: 1.1em; 554 | font-weight: bold; 555 | margin: -42px 0 15px; 556 | color: #696671; 557 | } 558 | .login-form-body { 559 | background: #fff; 560 | padding: 0; 561 | position: relative; 562 | perspective: 800px; 563 | perspective-origin: right top; 564 | } 565 | 566 | .login-form { 567 | background: rgba(255, 255, 255, 0.8); 568 | padding: 20px; 569 | border-top: 3px solid #3e4043; 570 | } 571 | 572 | .innter-form { 573 | padding: 0px 48px; 574 | background-color: #fff; 575 | border-radius: 4px; 576 | } 577 | 578 | .final-login { 579 | width: 100%; 580 | position: relative; 581 | height: 96px; 582 | padding: 0; 583 | margin: 0; 584 | z-index: 1; 585 | } 586 | .final-login li { 587 | list-style: none; 588 | z-index: 1; 589 | width: 96px; 590 | height: 96px; 591 | position: absolute; 592 | top: 0; 593 | right: 0; 594 | -webkit-clip-path: polygon(0 0, 96px 0, 96px 96px, 0 0); 595 | clip-path: polygon(0 0, 96px 0, 96px 96px, 0 0); 596 | } 597 | .final-login li a { 598 | -webkit-user-drag: none; 599 | position: absolute; 600 | width: 100%; 601 | height: 100%; 602 | top: 0; 603 | right: 0; 604 | -webkit-transition: all 250ms cubic-bezier(0.190, 1.000, 0.220, 1.000); 605 | -moz-transition: all 250ms cubic-bezier(0.190, 1.000, 0.220, 1.000); 606 | -o-transition: all 250ms cubic-bezier(0.190, 1.000, 0.220, 1.000); 607 | transition: all 250ms cubic-bezier(0.190, 1.000, 0.220, 1.000); 608 | } 609 | .final-login li#loginli a { 610 | background: url('../../resources/img/id-pw-icon.png'); 611 | } 612 | .final-login li#qrcodeloginli a { 613 | background: url('../../resources/img/qr-icon.png'); 614 | } 615 | .final-login li:hover a, .final-login li.hovered a { 616 | top: -5px; 617 | right: -5px; 618 | } 619 | .final-login li:hover:active a { 620 | top: 0px; 621 | right: 0px; 622 | } 623 | .final-login li.active { 624 | z-index: -1; 625 | opacity: 0; 626 | } 627 | .final-login li.active a { 628 | top: 0; 629 | right: 0; 630 | opacity: 0; 631 | } 632 | 633 | .final-login::before { 634 | content: ""; 635 | display: block; 636 | position: absolute; 637 | top: 0; 638 | right: 0; 639 | z-index: 1; 640 | width: 0; 641 | height: 0; 642 | border-top: 48px solid #FAE2E2; 643 | border-right: 48px solid #FAE2E2; 644 | border-left: 48px solid transparent; 645 | border-bottom: 48px solid transparent; 646 | } 647 | .final-login.final-login--hidden::before { 648 | display: none; 649 | } 650 | .white-area{ 651 | display: block; 652 | background: url("../../resources/img/qr-shadow.png") no-repeat 0 0; 653 | position: absolute; 654 | width: 96px; 655 | height: 96px; 656 | right: 0; 657 | top: 0; 658 | z-index: 2; 659 | cursor: pointer; 660 | pointer-events: none; 661 | } 662 | .white-area::after { 663 | content: ""; 664 | width: 0; 665 | height: 0; 666 | border-left: 48px solid #ffffff; 667 | border-bottom: 48px solid #ffffff; 668 | border-right: 48px solid transparent; 669 | border-top: 48px solid transparent; 670 | left: 0; 671 | top: 0; 672 | position: absolute; 673 | z-index: 2; 674 | } -------------------------------------------------------------------------------- /public/mockpass/resources/css/style-main.css: -------------------------------------------------------------------------------- 1 | /** Last Updated Date : 2018-12-28 11:00 AM */ 2 | @import "reset.css"; 3 | @import "style-baseline.css"; 4 | @import "style-homepage.css"; 5 | @import "style-common.css"; 6 | 7 | @import "style-baseline-small-media.css"; 8 | @import "style-homepage-small-media.css"; 9 | @import "style-common-small-media.css"; -------------------------------------------------------------------------------- /public/mockpass/resources/img/ajax-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengovsg/mockpass/8a4a7b3bb45ea0ea17db99ce67e61a916d878c4f/public/mockpass/resources/img/ajax-loader.gif -------------------------------------------------------------------------------- /public/mockpass/resources/img/ask_cheryl_tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengovsg/mockpass/8a4a7b3bb45ea0ea17db99ce67e61a916d878c4f/public/mockpass/resources/img/ask_cheryl_tab.png -------------------------------------------------------------------------------- /public/mockpass/resources/img/background/large-device/sp_bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengovsg/mockpass/8a4a7b3bb45ea0ea17db99ce67e61a916d878c4f/public/mockpass/resources/img/background/large-device/sp_bg.jpg -------------------------------------------------------------------------------- /public/mockpass/resources/img/background/medium-device/ipad-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengovsg/mockpass/8a4a7b3bb45ea0ea17db99ce67e61a916d878c4f/public/mockpass/resources/img/background/medium-device/ipad-bg.jpg -------------------------------------------------------------------------------- /public/mockpass/resources/img/background/medium-device/ipad-landscape-sp-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengovsg/mockpass/8a4a7b3bb45ea0ea17db99ce67e61a916d878c4f/public/mockpass/resources/img/background/medium-device/ipad-landscape-sp-bg.jpg -------------------------------------------------------------------------------- /public/mockpass/resources/img/background/small-device/mobile-sp-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengovsg/mockpass/8a4a7b3bb45ea0ea17db99ce67e61a916d878c4f/public/mockpass/resources/img/background/small-device/mobile-sp-bg.jpg -------------------------------------------------------------------------------- /public/mockpass/resources/img/carousel/large-device/how-to-setup-2fa-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengovsg/mockpass/8a4a7b3bb45ea0ea17db99ce67e61a916d878c4f/public/mockpass/resources/img/carousel/large-device/how-to-setup-2fa-icon.png -------------------------------------------------------------------------------- /public/mockpass/resources/img/carousel/large-device/register-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengovsg/mockpass/8a4a7b3bb45ea0ea17db99ce67e61a916d878c4f/public/mockpass/resources/img/carousel/large-device/register-icon.png -------------------------------------------------------------------------------- /public/mockpass/resources/img/carousel/large-device/reset-password-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengovsg/mockpass/8a4a7b3bb45ea0ea17db99ce67e61a916d878c4f/public/mockpass/resources/img/carousel/large-device/reset-password-icon.png -------------------------------------------------------------------------------- /public/mockpass/resources/img/carousel/large-device/setup-2fa-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengovsg/mockpass/8a4a7b3bb45ea0ea17db99ce67e61a916d878c4f/public/mockpass/resources/img/carousel/large-device/setup-2fa-icon.png -------------------------------------------------------------------------------- /public/mockpass/resources/img/carousel/large-device/update-acct-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengovsg/mockpass/8a4a7b3bb45ea0ea17db99ce67e61a916d878c4f/public/mockpass/resources/img/carousel/large-device/update-acct-icon.png -------------------------------------------------------------------------------- /public/mockpass/resources/img/carousel/medium-device/ipad-register-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengovsg/mockpass/8a4a7b3bb45ea0ea17db99ce67e61a916d878c4f/public/mockpass/resources/img/carousel/medium-device/ipad-register-icon.png -------------------------------------------------------------------------------- /public/mockpass/resources/img/carousel/medium-device/ipad-reset-password-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengovsg/mockpass/8a4a7b3bb45ea0ea17db99ce67e61a916d878c4f/public/mockpass/resources/img/carousel/medium-device/ipad-reset-password-icon.png -------------------------------------------------------------------------------- /public/mockpass/resources/img/carousel/medium-device/ipad-setup-2fa-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengovsg/mockpass/8a4a7b3bb45ea0ea17db99ce67e61a916d878c4f/public/mockpass/resources/img/carousel/medium-device/ipad-setup-2fa-icon.png -------------------------------------------------------------------------------- /public/mockpass/resources/img/carousel/medium-device/ipad-update-acct-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengovsg/mockpass/8a4a7b3bb45ea0ea17db99ce67e61a916d878c4f/public/mockpass/resources/img/carousel/medium-device/ipad-update-acct-icon.png -------------------------------------------------------------------------------- /public/mockpass/resources/img/carousel/small-device/mobile-register.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengovsg/mockpass/8a4a7b3bb45ea0ea17db99ce67e61a916d878c4f/public/mockpass/resources/img/carousel/small-device/mobile-register.png -------------------------------------------------------------------------------- /public/mockpass/resources/img/carousel/small-device/mobile-reset-password-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengovsg/mockpass/8a4a7b3bb45ea0ea17db99ce67e61a916d878c4f/public/mockpass/resources/img/carousel/small-device/mobile-reset-password-icon.png -------------------------------------------------------------------------------- /public/mockpass/resources/img/carousel/small-device/mobile-update-acct-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengovsg/mockpass/8a4a7b3bb45ea0ea17db99ce67e61a916d878c4f/public/mockpass/resources/img/carousel/small-device/mobile-update-acct-icon.png -------------------------------------------------------------------------------- /public/mockpass/resources/img/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengovsg/mockpass/8a4a7b3bb45ea0ea17db99ce67e61a916d878c4f/public/mockpass/resources/img/close.png -------------------------------------------------------------------------------- /public/mockpass/resources/img/id-pw-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengovsg/mockpass/8a4a7b3bb45ea0ea17db99ce67e61a916d878c4f/public/mockpass/resources/img/id-pw-icon.png -------------------------------------------------------------------------------- /public/mockpass/resources/img/logo/mockpass-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengovsg/mockpass/8a4a7b3bb45ea0ea17db99ce67e61a916d878c4f/public/mockpass/resources/img/logo/mockpass-logo.png -------------------------------------------------------------------------------- /public/mockpass/resources/img/logo/mockpass-placeholder-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengovsg/mockpass/8a4a7b3bb45ea0ea17db99ce67e61a916d878c4f/public/mockpass/resources/img/logo/mockpass-placeholder-logo.png -------------------------------------------------------------------------------- /public/mockpass/resources/img/logo/mockpass_watermark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengovsg/mockpass/8a4a7b3bb45ea0ea17db99ce67e61a916d878c4f/public/mockpass/resources/img/logo/mockpass_watermark.png -------------------------------------------------------------------------------- /public/mockpass/resources/img/qr-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengovsg/mockpass/8a4a7b3bb45ea0ea17db99ce67e61a916d878c4f/public/mockpass/resources/img/qr-icon.png -------------------------------------------------------------------------------- /public/mockpass/resources/img/qr-shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengovsg/mockpass/8a4a7b3bb45ea0ea17db99ce67e61a916d878c4f/public/mockpass/resources/img/qr-shadow.png -------------------------------------------------------------------------------- /public/mockpass/resources/img/refresh.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengovsg/mockpass/8a4a7b3bb45ea0ea17db99ce67e61a916d878c4f/public/mockpass/resources/img/refresh.jpg -------------------------------------------------------------------------------- /public/mockpass/resources/img/sidebar-icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengovsg/mockpass/8a4a7b3bb45ea0ea17db99ce67e61a916d878c4f/public/mockpass/resources/img/sidebar-icons.png -------------------------------------------------------------------------------- /public/mockpass/resources/img/sp-qr-unavailable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengovsg/mockpass/8a4a7b3bb45ea0ea17db99ce67e61a916d878c4f/public/mockpass/resources/img/sp-qr-unavailable.png -------------------------------------------------------------------------------- /public/mockpass/resources/img/utility-icon-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengovsg/mockpass/8a4a7b3bb45ea0ea17db99ce67e61a916d878c4f/public/mockpass/resources/img/utility-icon-black.png -------------------------------------------------------------------------------- /public/mockpass/resources/plugins/bootstrap-3.3.6/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengovsg/mockpass/8a4a7b3bb45ea0ea17db99ce67e61a916d878c4f/public/mockpass/resources/plugins/bootstrap-3.3.6/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /static/certs/csr.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIICujCCAaICAQAwdTELMAkGA1UEBhMCU0cxEjAQBgNVBAgMCVNpbmdhcG9yZTES 3 | MBAGA1UEBwwJU2luZ2Fwb3JlMSMwIQYDVQQKDBpUZXN0cyBGb3Igc3BjcC1hdXRo 4 | LWNsaWVudDEZMBcGA1UECwwQc3BjcC1hdXRoLWNsaWVudDCCASIwDQYJKoZIhvcN 5 | AQEBBQADggEPADCCAQoCggEBALt2LxMYdaoyQaZIwCynwrgeuyqo8wnPxMFKwTpH 6 | aBGDS7q/NANmN0eL9qtQFvH/Ht2zIjyy0pb8UtGtGMmok9vuPxDY8MvnVGtaPV3Y 7 | li5h1ljFou43tSCuM7A8aCLfZ0jsWmZobuGmXj/hoazIlS51dMSg5f+J9kGWw9Rp 8 | HZBuGbsqhybJhnBrVy1NVOq+qemQ6wk5AV41/ehA6Gx311RX4v0Se1Tg9mj9qeBu 9 | yNdOLTQWJgffppB6+ALjk/T4ZjqquGvRaNROlS55aOBVv9mStdIFFNH+cGnJzYpZ 10 | REdDRjcBVqE49gEhfS4kev9/W+LGrUSwMvEcTHJb2vIiuskCAwEAAaAAMA0GCSqG 11 | SIb3DQEBCwUAA4IBAQCqoY+YKjEPx2gtFsXmHQVJsEGpRkXnuX+1lDlJUnVj6AAD 12 | NTl3p3cUk1fPsTad+OKpaXFNYN6d+pMCRXqPKZtJ7zbkhxY8XbZIYXbZvvruENyv 13 | Jwu6P23A9dHQkM//7c4YtbtBo9EwUYGLpcocOIJyYxP7aP7K/QcBN/z5OyEd1vFR 14 | s56rf/9zioONCaHQnom5C0AvP6E3o+ljO9DOAUDBF26Cpyu65Ps1zMuJF4V0uaY8 15 | JwUug7Z+ukf4T+E04aARjBHwJ2jl9NDgOek1PZtUwEfVma8UzeQpaoee4PgzvMc3 16 | POy8g/Mx6cj+mQT5GYo5fWSN5dVZOhwqQuZoI64U 17 | -----END CERTIFICATE REQUEST----- 18 | -------------------------------------------------------------------------------- /static/certs/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC7di8TGHWqMkGm 3 | SMAsp8K4HrsqqPMJz8TBSsE6R2gRg0u6vzQDZjdHi/arUBbx/x7dsyI8stKW/FLR 4 | rRjJqJPb7j8Q2PDL51RrWj1d2JYuYdZYxaLuN7UgrjOwPGgi32dI7FpmaG7hpl4/ 5 | 4aGsyJUudXTEoOX/ifZBlsPUaR2Qbhm7KocmyYZwa1ctTVTqvqnpkOsJOQFeNf3o 6 | QOhsd9dUV+L9EntU4PZo/angbsjXTi00FiYH36aQevgC45P0+GY6qrhr0WjUTpUu 7 | eWjgVb/ZkrXSBRTR/nBpyc2KWURHQ0Y3AVahOPYBIX0uJHr/f1vixq1EsDLxHExy 8 | W9ryIrrJAgMBAAECggEAY8jsE+kQMRFhWqcdDGgcQS+yh2m5PP7Ih+9H3cLGxZOz 9 | CuvePvT49e+t1NDj9drMTkydK9wwNsiHOS8/o5BFbGtsTIZ93rv7ds1pHvw8LOJN 10 | W6GQMeebVZME1onBENcEPo/5KsvqQdjyEGUFT1jR+BHznvrakuSYHZ+oC/gMEaVw 11 | dUsM1849EbLDJ5lOPDDqYwsJwIGEryxRLFP+4HhGR9wnrTVee5CCsH0ep8OqypvY 12 | xgg9Ytyt1WripwzhXsVzJxahTbO4XImOgv6Uvo3EYfBXm1gbfugGbSiyYHBBx0Pa 13 | 7DtYrpRzjeS33m6Y4SJjKERjCbHXChwUrFcMGS5ogQKBgQDeChbIHYRu4HTQYoV9 14 | pJ70LcHCiE0zaJzuMHes4OFkNAuFXASDGp1HJXd0tol+oeuYO/q1gENvyfBAqDH/ 15 | AVAtWaFvQUNzv/Zs++mbrSSwxuZJCaJAbh9GKzCyCbapxBtdzhgCLLY9GeWRiG1c 16 | puYUUvJmkcWQE9sSn2tnE8mv2QKBgQDYIjWgagTLPlT3J65t5BEs9fwjAq8bdIIV 17 | E9o0blIwlKF3FzeUo4Egjc/vfIYOL43AOttuVbVoruvvT13a3+VpCXjxqSYnveuw 18 | FB0xQC1F0c3ay4Oprjh2qk2GfOy0gikaRgmTUP3nVK1LC57GWJMker/UQ2kWCJCs 19 | RyMzpgd8cQKBgQCE5q8KKrjJEOp6jG3wbWeDKhwuzxy+Z6B+5V3MiXH/YzN+KDy/ 20 | KF/5ZNCieFvGAy8cGNKQbuxubgWy/bmnM+cErgB1si+oib77LrF+L92lPfg6wVxv 21 | ijqH6nQkLLI73RiwRhqSuqZ93hFN0cX7zh4rDhbvE9OX0HqxI+DKesqeyQKBgEln 22 | hPMQTsSATPcMAQ/Nb4/nk1SIqtQWQ7/I2EkKVtus/xGlTvkqdsaJo19g2V6kA+6P 23 | jsrwTQZaskK6n9OgSxfbYbohipXgyNUqX6fEdhvKX7G5gOP2CbMzr9THRNUhh7gm 24 | pUXlMfaJKbndHnWay46OKex7YItdKVV5a5k1AEHhAoGBAIVSqdn2f4mVgdQxDls6 25 | DPp0XJW2D9nI9w9mm+wZc9TtI7X+7+1LZ609vDHAEfj8flTMl0t/ovaaZWmEHCpg 26 | FKM9rIqep3tZy6qqgo63peDk+RPV3UVgnUwq0+mBwhnHPWQf2nfYnZZHwsldfHWP 27 | y0U6Qc7UNI3EjJEJPTI4Zifg 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /static/certs/key.pub: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu3YvExh1qjJBpkjALKfC 3 | uB67KqjzCc/EwUrBOkdoEYNLur80A2Y3R4v2q1AW8f8e3bMiPLLSlvxS0a0YyaiT 4 | 2+4/ENjwy+dUa1o9XdiWLmHWWMWi7je1IK4zsDxoIt9nSOxaZmhu4aZeP+GhrMiV 5 | LnV0xKDl/4n2QZbD1GkdkG4ZuyqHJsmGcGtXLU1U6r6p6ZDrCTkBXjX96EDobHfX 6 | VFfi/RJ7VOD2aP2p4G7I104tNBYmB9+mkHr4AuOT9PhmOqq4a9Fo1E6VLnlo4FW/ 7 | 2ZK10gUU0f5wacnNillER0NGNwFWoTj2ASF9LiR6/39b4satRLAy8RxMclva8iK6 8 | yQIDAQAB 9 | -----END PUBLIC KEY----- 10 | -------------------------------------------------------------------------------- /static/certs/oidc-v2-asp-public.json: -------------------------------------------------------------------------------- 1 | { 2 | "keys": [ 3 | { 4 | "kty": "EC", 5 | "use": "sig", 6 | "crv": "P-521", 7 | "kid": "sig-1655709297", 8 | "x": "AWuSHLkeP89DOkPaTs6MUDTFX1oL_Nr2rsJxCUyWL9x4LDEwtGXxWmw5-KhJSKauwJL2fAiNribZa2E0EZ-A4DzL", 9 | "y": "AHoghl5OGyp7Vejt2sqYW7z2G_gTGBDR9q-ylLjnERpKd7-kHybLEutkwp5tmkhhlOysCcXE7vpTcnwxeQPa3zN0" 10 | }, 11 | { 12 | "kty": "EC", 13 | "use": "sig", 14 | "crv": "P-256", 15 | "kid": "ndi_mock_01", 16 | "x": "ZyAP_T3GS6tzdEfIKgj7Z_TkKWQ9AQAU7LNTSV_JICQ", 17 | "y": "gxQgPvGD8ASZT7DT41pgWP4ZHiZ_7HGcMoDM0NEOfO8", 18 | "alg": "ES256" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /static/certs/oidc-v2-asp-secret.json: -------------------------------------------------------------------------------- 1 | { 2 | "keys": [ 3 | { 4 | "kty": "EC", 5 | "d": "ATdzXBC0WOU74xmdFfeVWfm2ybggXGeWGCMpYlpqzhW5cdrTrlj7UbTmKYlPJe70F5UD-wG2TK6tUoNiVpfEKmky", 6 | "use": "sig", 7 | "crv": "P-521", 8 | "kid": "sig-1655709297", 9 | "x": "AWuSHLkeP89DOkPaTs6MUDTFX1oL_Nr2rsJxCUyWL9x4LDEwtGXxWmw5-KhJSKauwJL2fAiNribZa2E0EZ-A4DzL", 10 | "y": "AHoghl5OGyp7Vejt2sqYW7z2G_gTGBDR9q-ylLjnERpKd7-kHybLEutkwp5tmkhhlOysCcXE7vpTcnwxeQPa3zN0" 11 | }, 12 | { 13 | "kty": "EC", 14 | "d": "_nXJySWym8zFj_jL3skM2zf0wxL8GQo10WgC3nrx3vw", 15 | "use": "sig", 16 | "crv": "P-256", 17 | "kid": "ndi_mock_01", 18 | "x": "ZyAP_T3GS6tzdEfIKgj7Z_TkKWQ9AQAU7LNTSV_JICQ", 19 | "y": "gxQgPvGD8ASZT7DT41pgWP4ZHiZ_7HGcMoDM0NEOfO8" 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /static/certs/oidc-v2-rp-public.json: -------------------------------------------------------------------------------- 1 | { 2 | "keys": [ 3 | { 4 | "kty": "EC", 5 | "use": "sig", 6 | "crv": "P-521", 7 | "kid": "sig-2022-06-04T09:22:28Z", 8 | "x": "AAj_CAKL9NmP6agPCMto6_LiYQqko3o3ZWTtBg75bA__Z8yKEv_CwHzaibkVLnJ9XKWxCQeyEk9ROLhJoJuZxnsI", 9 | "y": "AZeoe0v-EwqD3oo1V5lxUAmC80qHt-ybqOsl1mYKPgE_ctGcD4hj8tVhmD0Of6ARuKVTxNWej-X82hEW_7Aa-XpR", 10 | "alg": "ES512" 11 | }, 12 | { 13 | "kty": "EC", 14 | "use": "enc", 15 | "crv": "P-521", 16 | "kid": "enc-2022-06-04T13:46:15Z", 17 | "x": "AB-16HyJwnlSZbQtqhFskADqFrm6rgX9XeaV8FgynX61750GCRbYjoueDosSNt-qzK5QNHskdQw0QZ700YF2JIlb", 18 | "y": "AZwYlSBSdV-CxGRMz6ovTvWxKJ6e44gaZHf-YfbJV7w9VdAJb3OuzbHNGRuzNDjEa8eH-paLDaAB84ezrEm1SRHq", 19 | "alg": "ECDH-ES+A256KW" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /static/certs/oidc-v2-rp-secret.json: -------------------------------------------------------------------------------- 1 | { 2 | "keys": [ 3 | { 4 | "kty": "EC", 5 | "d": "AFOzlND2sq43ykty-VZXw-IEIOyHkBsNXUU77o5yEYcktpoMe9Dl3jsaXwzRK6wtDJH_uoz4IG1Uj4J_WyH5O3GS", 6 | "use": "sig", 7 | "crv": "P-521", 8 | "kid": "sig-2022-06-04T09:22:28Z", 9 | "x": "AAj_CAKL9NmP6agPCMto6_LiYQqko3o3ZWTtBg75bA__Z8yKEv_CwHzaibkVLnJ9XKWxCQeyEk9ROLhJoJuZxnsI", 10 | "y": "AZeoe0v-EwqD3oo1V5lxUAmC80qHt-ybqOsl1mYKPgE_ctGcD4hj8tVhmD0Of6ARuKVTxNWej-X82hEW_7Aa-XpR", 11 | "alg": "ES512" 12 | }, 13 | { 14 | "kty": "EC", 15 | "d": "AP7xECOnlKW-FuLpe1h3ULZoqFzScFrbyAEQTFFG49j5HRHl0k13-6_6nWnwJ9Y8sTrGOWH4GszmDBBZGGvESJQr", 16 | "use": "enc", 17 | "crv": "P-521", 18 | "kid": "enc-2022-06-04T13:46:15Z", 19 | "x": "AB-16HyJwnlSZbQtqhFskADqFrm6rgX9XeaV8FgynX61750GCRbYjoueDosSNt-qzK5QNHskdQw0QZ700YF2JIlb", 20 | "y": "AZwYlSBSdV-CxGRMz6ovTvWxKJ6e44gaZHf-YfbJV7w9VdAJb3OuzbHNGRuzNDjEa8eH-paLDaAB84ezrEm1SRHq", 21 | "alg": "ECDH-ES+A256KW" 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /static/certs/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDaDCCAlACCQDR6jPZhsHgXDANBgkqhkiG9w0BAQUFADB1MQswCQYDVQQGEwJT 3 | RzESMBAGA1UECAwJU2luZ2Fwb3JlMRIwEAYDVQQHDAlTaW5nYXBvcmUxIzAhBgNV 4 | BAoMGlRlc3RzIEZvciBzcGNwLWF1dGgtY2xpZW50MRkwFwYDVQQLDBBzcGNwLWF1 5 | dGgtY2xpZW50MCAXDTE4MDgxNTA2MTEyOFoYDzIwODQwNDMwMDYxMTI4WjB1MQsw 6 | CQYDVQQGEwJTRzESMBAGA1UECAwJU2luZ2Fwb3JlMRIwEAYDVQQHDAlTaW5nYXBv 7 | cmUxIzAhBgNVBAoMGlRlc3RzIEZvciBzcGNwLWF1dGgtY2xpZW50MRkwFwYDVQQL 8 | DBBzcGNwLWF1dGgtY2xpZW50MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC 9 | AQEAu3YvExh1qjJBpkjALKfCuB67KqjzCc/EwUrBOkdoEYNLur80A2Y3R4v2q1AW 10 | 8f8e3bMiPLLSlvxS0a0YyaiT2+4/ENjwy+dUa1o9XdiWLmHWWMWi7je1IK4zsDxo 11 | It9nSOxaZmhu4aZeP+GhrMiVLnV0xKDl/4n2QZbD1GkdkG4ZuyqHJsmGcGtXLU1U 12 | 6r6p6ZDrCTkBXjX96EDobHfXVFfi/RJ7VOD2aP2p4G7I104tNBYmB9+mkHr4AuOT 13 | 9PhmOqq4a9Fo1E6VLnlo4FW/2ZK10gUU0f5wacnNillER0NGNwFWoTj2ASF9LiR6 14 | /39b4satRLAy8RxMclva8iK6yQIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQA4OIt/ 15 | HwN/wmOiW3yeV+HaVlK5yMpJ1qMdxqTRagvbwDoXJbYtDvU4yFd4LrwF8lbtJ3Ne 16 | M4PJFQGu3DVXqm9mZqcBGPBuQfaqww+aD3h94WCFUG/A+vswSC7o68/vTLjshCLT 17 | 8yVtfTtI3KoNq67D60M56oPNZZo8fS9zWr+MzYLaCmQpKPwmEdzC2kcxSvXGTZ2E 18 | yh+JM+ExaL7OHqMdE4lo1pfx9Nuc3QIjpowmsjXtl5LtPHiNhYOjtw1Js0y1jEmC 19 | Kwq/AMTKwQV0zD7+wbdGmJjF16KRM8W3b+fDIdlqhhBFrTNK/wTVV3sWN2AE/NsN 20 | 4N0sc7KtEgUWl5Fc 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /static/certs/spcp-csr.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIICsTCCAZkCAQAwbDELMAkGA1UEBhMCU0cxEjAQBgNVBAgMCVNpbmdhcG9yZTES 3 | MBAGA1UEBwwJU2luZ2Fwb3JlMR4wHAYDVQQKDBVTaW5nUGFzcyBhbmQgQ29ycFBh 4 | c3MxFTATBgNVBAsMDE1vY2sgU2VydmljZTCCASIwDQYJKoZIhvcNAQEBBQADggEP 5 | ADCCAQoCggEBAK+H5VSzv0bAxZoKjnbjHmzARoi3Hdvs1+lXZHa51pePzTHocnEk 6 | e2krjKpA44v3arKGN0NVR1lGKFhi6wI2fyT/wHdr8CNQ90iSxuMWSrRmsDNYNw5B 7 | vWNnn3IZNa+/lqzBJ/NQzSdYm/uOtoNcjZZYMXgwkWiJrIIoY87VhUXD7v3byxbu 8 | 51Qy9J07DfEqpXYdsnTJTZ5l4sS+txVGhGB7GMpZv+tLMBpWt4m4Tn8wm5o5DeOL 9 | 65LQZGR4bCNBteUBeb59DW8+w3x6diLsWV8E5zYI89UEqVdfpMR6LCRU2tg9U0tS 10 | ovRYDCI9Gr9E6itjJD6uaZ05tF72fzuh8OMCAwEAAaAAMA0GCSqGSIb3DQEBCwUA 11 | A4IBAQB5zpKot0LcLJy74DHFKxUVVEwjGzaXhesQLWN2wZ7i2huJgT1RRH6/rAMm 12 | PfS4gCQ/7mcFPXYAyHZMhBTAUoTKf2xM0Vshr8TGZHJVwPUIRdpO2RyVT+MegJLt 13 | S5EJBTC9MKyR8ttAXc2p9pldDYz3rfWc68PqiOuwmEYhmiBJKy0weSKtw18PHbnM 14 | 6mXaRjDV77drO+pFDLMuyoX5yY4n8KWx+zTKQ96Bze5bpAbuQzMGJNFimBpkD2je 15 | kfhjBJu6UcuIMx0DU0IamZOdT2FYxsu+kbeD+pZ/L5lNChuU22vJv5U961rggyXI 16 | 1gae0TSwgQDTOplYLd/IvsGj2MnR 17 | -----END CERTIFICATE REQUEST----- 18 | -------------------------------------------------------------------------------- /static/certs/spcp-key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCvh+VUs79GwMWa 3 | Co524x5swEaItx3b7NfpV2R2udaXj80x6HJxJHtpK4yqQOOL92qyhjdDVUdZRihY 4 | YusCNn8k/8B3a/AjUPdIksbjFkq0ZrAzWDcOQb1jZ59yGTWvv5aswSfzUM0nWJv7 5 | jraDXI2WWDF4MJFoiayCKGPO1YVFw+7928sW7udUMvSdOw3xKqV2HbJ0yU2eZeLE 6 | vrcVRoRgexjKWb/rSzAaVreJuE5/MJuaOQ3ji+uS0GRkeGwjQbXlAXm+fQ1vPsN8 7 | enYi7FlfBOc2CPPVBKlXX6TEeiwkVNrYPVNLUqL0WAwiPRq/ROorYyQ+rmmdObRe 8 | 9n87ofDjAgMBAAECggEAXXwrD6mLvcr9csUciwT7N0BQUI/2PyMs+wGoZ/Mh7yaP 9 | Sn1aNhgQAjtHd4WHqwvir6H73MiWb12GL0y/jTYpETOE9hVul+CPUv+ZHWjJ8Lqg 10 | LThWWil5DHAr40C57xhCz08wT85A9SukJ54iZmPspJ3j+vci+mIYllmcjpP5nuWQ 11 | v5ZhqfZMrA0YfAINKkJrLMuNGxur3RPx2tu5uXt1UT4nkr2Z8QpOaR76+Cw/Vm6m 12 | 9LJ4BgYjDAodkU6P7Y70A8P9AHx/gwVB2VBGc+VXcBg+tf/x3ro5HGkaOgb2SFRJ 13 | PbJvFrVYH6t4cbYFDv5JfRXL73wWo8KAypY8pPb/AQKBgQDnc5pJ3SmXFQDEQniy 14 | sJ+ErJ2b6EpO9ETw0ll2CmPYfdR8rhfc9hovhaJn/B/zBx5khBQcr52oMEVh+FnV 15 | zif7LnnKAe6T0pxIzEceHeXmJhP3BKMzWZtDLpLbiqVVGSkP07ro/q41rBOghB88 16 | YS14wKCyY4x4YrX3AIHsde+08wKBgQDCJe15AjShYKDCJ4RjBjktd3tSSj781xNI 17 | F0LlPHyV2QCrmBUzS0ulFMC4S2pNA+ixOUfCunyK8+cAG+FSPStSBDdLuJ1C3xQn 18 | 93myd1r6BKnzMsHBbj0KsdlaP4OhmIA3FTR1nYXdm8tfYAyvkQ5JQilahe+c2s6T 19 | cAGiWzuQUQKBgQDhPxwUbmwfYI1Scu5L2KAl2me4ZySKGidNxyjRO+NXuX2lqTgI 20 | DmoFfaREVpYxSehGIlQAZtij6fZcFfo3nV5DkUNtWNv6eKkoH8XGhYpLpRsg9x5s 21 | xvPXOegqSJAGdWoEwSXRwqmACms/d9V+SYSbU7wQX9lA/6/fJltK6KvUCQKBgAd2 22 | W71V913oj+VGjZEc0R/NQuEz113yilwwALM88vDziVIPI2l4UG0E8i9jPq+9IbmG 23 | IRr7/gN9Qni/mZaGoV6iqNlxPCIw3t52ZagVbFrFyR5+6fGcYh5CHb+ZR17ztKHp 24 | X73Rky6kaVm+IF6zLaBlOZ+wHDikNGJ4YKez6AMxAoGBAJXvCcinHMHVdb6sFKn6 25 | U2qg+ZDuMOsFqBxPAUCyUCuKBBzNHesD+soDB2fLvK1+lJKRFhMxhkzWBCoUbbrg 26 | rYzNzlY5vOYT4s7j4lrYjazbVGHgWFCSlMJxtC3nRV8CaUjYausNl2qUZQgLolH3 27 | 8+CB6fajwuUp18h9M+GIoNhl 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /static/certs/spcp.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDVjCCAj4CCQCsiLHZmKY1QjANBgkqhkiG9w0BAQUFADBsMQswCQYDVQQGEwJT 3 | RzESMBAGA1UECAwJU2luZ2Fwb3JlMRIwEAYDVQQHDAlTaW5nYXBvcmUxHjAcBgNV 4 | BAoMFVNpbmdQYXNzIGFuZCBDb3JwUGFzczEVMBMGA1UECwwMTW9jayBTZXJ2aWNl 5 | MCAXDTE4MDgxNTA2MTE1MFoYDzIwODQwNDMwMDYxMTUwWjBsMQswCQYDVQQGEwJT 6 | RzESMBAGA1UECAwJU2luZ2Fwb3JlMRIwEAYDVQQHDAlTaW5nYXBvcmUxHjAcBgNV 7 | BAoMFVNpbmdQYXNzIGFuZCBDb3JwUGFzczEVMBMGA1UECwwMTW9jayBTZXJ2aWNl 8 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr4flVLO/RsDFmgqOduMe 9 | bMBGiLcd2+zX6VdkdrnWl4/NMehycSR7aSuMqkDji/dqsoY3Q1VHWUYoWGLrAjZ/ 10 | JP/Ad2vwI1D3SJLG4xZKtGawM1g3DkG9Y2efchk1r7+WrMEn81DNJ1ib+462g1yN 11 | llgxeDCRaImsgihjztWFRcPu/dvLFu7nVDL0nTsN8Sqldh2ydMlNnmXixL63FUaE 12 | YHsYylm/60swGla3ibhOfzCbmjkN44vrktBkZHhsI0G15QF5vn0Nbz7DfHp2IuxZ 13 | XwTnNgjz1QSpV1+kxHosJFTa2D1TS1Ki9FgMIj0av0TqK2MkPq5pnTm0XvZ/O6Hw 14 | 4wIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQAnuQ3AIqdZP5N0aKyspTwNRaV2fk6W 15 | iGO39Bt9ehxydZapzpNsDCFQdTSmNbQQngxrN8zSXZH2D8bihM49aBkMnZtSE1Ti 16 | WJ++kjftuX/T1H1QJJo+RDl1riZJKZj9Jh+xASJgVObA4VEDnRLvAb72PWpCupqr 17 | m1R97ippgbkraKamY+plATK+/eqEgBTxK+PAZrHhodtdtCZRQRoHCuLPPo/xQ/P8 18 | a/SGMBNsIuaWX+ulWO/jfqgdzJl4njTgPRFJC80iyOfu3CZSsKLOXbrSWhz4+nub 19 | GZjcicSwxkT6P5/R85+AO81AYUlwuy7hrEJfouQr3syYRIge/COJaNDI 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /static/html/consent.html: -------------------------------------------------------------------------------- 1 | 2 | MyInfo - Consent to giving details 3 | 4 | Disclose the following details of {{ id }} to this provider? 5 |
6 |
7 | 8 | 9 |
10 |
11 | 12 | 13 |
14 |
15 | 16 | 17 |
18 |
19 | 20 | 21 |
22 |
23 | 24 | 25 |
26 |
27 | Decision: 28 |
29 | 30 | 31 |
32 |
33 | 34 | 35 |
36 |
37 | 38 |
39 | 40 | --------------------------------------------------------------------------------