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 |
39 |
40 |
--------------------------------------------------------------------------------