@opengovsg/mockpass 4.0.11 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.husky/pre-push +1 -1
- package/README.md +148 -69
- package/app.js +62 -0
- package/index.js +1 -56
- package/lib/auth-code.js +14 -4
- package/lib/express/myinfo/consent.js +2 -2
- package/lib/express/oidc/spcp.js +20 -8
- package/lib/express/oidc/v2-ndi.js +5 -5
- package/lib/express/sgid.js +15 -5
- package/package.json +3 -3
package/.husky/pre-push
CHANGED
package/README.md
CHANGED
|
@@ -1,107 +1,186 @@
|
|
|
1
1
|
# MockPass
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A mock Singpass/Corppass/Myinfo v3/sgID v2 server for dev purposes
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Quick Start (hosted remotely on Gitpod)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
[](https://gitpod.io/#https://github.com/opengovsg/mockpass)
|
|
8
8
|
|
|
9
9
|
- Click the ready-to-code badge above
|
|
10
10
|
- Wait for MockPass to start
|
|
11
11
|
- Make port 5156 public
|
|
12
|
-
- Open
|
|
12
|
+
- Open browser to note the URL hosting MockPass
|
|
13
13
|
- Configure your application per local machine quick start, changing
|
|
14
|
-
the localhost:5156 to the Gitpod hostname
|
|
14
|
+
the `localhost:5156` to the Gitpod hostname
|
|
15
|
+
|
|
16
|
+
## Quick Start (hosted locally)
|
|
17
|
+
|
|
18
|
+
### Singpass v2 (NDI OIDC)
|
|
19
|
+
|
|
20
|
+
Configure your application to point to the following endpoints:
|
|
21
|
+
|
|
22
|
+
- http://localhost:5156/singpass/v2/.well-known/openid-configuration
|
|
23
|
+
- http://localhost:5156/singpass/v2/.well-known/keys
|
|
24
|
+
- http://localhost:5156/singpass/v2/authorize
|
|
25
|
+
- http://localhost:5156/singpass/v2/token
|
|
26
|
+
|
|
27
|
+
Configure your application (or MockPass) with keys:
|
|
28
|
+
|
|
29
|
+
- EITHER configure MockPass with your application's JWKS endpoint URL using the env
|
|
30
|
+
var `SP_RP_JWKS_ENDPOINT`.
|
|
31
|
+
- OR configure your application to use the private keys from
|
|
32
|
+
`static/certs/oidc-v2-rp-secret.json`.
|
|
33
|
+
|
|
34
|
+
MockPass accepts any value for `client_id` and `redirect_uri`.
|
|
35
|
+
|
|
36
|
+
| Configuration item | Explanation |
|
|
37
|
+
|---|---|
|
|
38
|
+
| Client signing and encryption keys | **Overview:** When client makes any request, what signing key is used to verify the client's signature on the client assertion, and what encryption key is used to encrypt the data payload. <br> **Default:** static keyset `static/certs/oidc-v2-rp-public.json` is used. <br> **How to configure:** Set the env var `SP_RP_JWKS_ENDPOINT` to a JWKS URL that MockPass can connect to. This can be a HTTP or HTTPS URL. |
|
|
39
|
+
| Login page | **Overview:** When client makes an authorize request, whether MockPass sends the client to a login page, instead of completing the login silently. <br> **Default:** Disabled for all requests. <br> **How to configure:** Enable for all requests by default by setting the env var `SHOW_LOGIN_PAGE` to `true`. Regardless of the default, you can override on a per-request basis by sending the HTTP request header `X-Show-Login-Page` with the value `true`. <br> **Detailed effect:** When login page is disabled, MockPass will immediately complete login and redirect to the `redirect_uri`. The profile used will be (in order of decreasing precedence) the profile specified in HTTP request headers (`X-Custom-NRIC` and `X-Custom-UUID` must both be specified), the profile with the NRIC specified in the env var `MOCKPASS_NRIC`, or the first profile in MockPass' static data. <br> When login page is enabled, MockPass returns a HTML page with a form that is used to complete the login. The client may select an existing profile, or provide a custom NRIC and UUID on the form. |
|
|
40
|
+
|
|
41
|
+
### Corppass v2 (Corppass OIDC)
|
|
42
|
+
|
|
43
|
+
Configure your application to point to the following endpoints:
|
|
44
|
+
|
|
45
|
+
- http://localhost:5156/corppass/v2/.well-known/openid-configuration
|
|
46
|
+
- http://localhost:5156/corppass/v2/.well-known/keys
|
|
47
|
+
- http://localhost:5156/corppass/v2/authorize
|
|
48
|
+
- http://localhost:5156/corppass/v2/token
|
|
49
|
+
|
|
50
|
+
Configure your application (or MockPass) with keys:
|
|
51
|
+
|
|
52
|
+
- EITHER configure MockPass with your application's JWKS endpoint URL using the env
|
|
53
|
+
var `CP_RP_JWKS_ENDPOINT`. HTTP/HTTPS endpoints are supported.
|
|
54
|
+
- OR configure your application to use the private keys from
|
|
55
|
+
`static/certs/oidc-v2-rp-secret.json`.
|
|
56
|
+
|
|
57
|
+
MockPass accepts any value for `client_id` and `redirect_uri`.
|
|
58
|
+
|
|
59
|
+
| Configuration item | Explanation |
|
|
60
|
+
|---|---|
|
|
61
|
+
| Client signing and encryption keys | **Overview:** When client makes any request, what signing key is used to verify the client's signature on the client assertion, and what encryption key is used to encrypt the data payload. <br> **Default:** static keyset `static/certs/oidc-v2-rp-public.json` is used. <br> **How to configure:** Set the env var `CP_RP_JWKS_ENDPOINT` to a JWKS URL that MockPass can connect to. This can be a HTTP or HTTPS URL. |
|
|
62
|
+
| Login page | **Overview:** When client makes an authorize request, whether MockPass sends the client to a login page, instead of completing the login silently. <br> **Default:** Disabled for all requests. <br> **How to configure:** Enable for all requests by default by setting the env var `SHOW_LOGIN_PAGE` to `true`. Regardless of the default, you can override on a per-request basis by sending the HTTP request header `X-Show-Login-Page` with the value `true`. <br> **Detailed effect:** When login page is disabled, MockPass will immediately complete login and redirect to the `redirect_uri`. The profile used will be (in order of decreasing precedence) the profile specified in HTTP request headers (`X-Custom-NRIC`, `X-Custom-UUID`, `X-Custom-UEN` must all be specified), the profile with the NRIC specified in the env var `MOCKPASS_NRIC`, or the first profile in MockPass' static data. <br> When login page is enabled, MockPass returns a HTML page with a form that is used to complete the login. The client may select an existing profile, or provide a custom NRIC, UUID and UEN on the form. |
|
|
63
|
+
|
|
64
|
+
### Myinfo v3
|
|
65
|
+
|
|
66
|
+
Configure your application to point to the following endpoints:
|
|
67
|
+
|
|
68
|
+
- http://localhost:5156/myinfo/v3/authorise
|
|
69
|
+
- http://localhost:5156/myinfo/v3/token
|
|
70
|
+
- http://localhost:5156/myinfo/v3/person-basic (exclusive to government systems)
|
|
71
|
+
- http://localhost:5156/myinfo/v3/person
|
|
72
|
+
|
|
73
|
+
Configure your application (or MockPass) with certificates/keys:
|
|
74
|
+
|
|
75
|
+
- Provide your application with
|
|
76
|
+
the certificate `static/certs/spcp.crt` as the Myinfo public certificate.
|
|
77
|
+
- EITHER configure MockPass with your application's X.509 certificate by setting
|
|
78
|
+
the env vars `SERVICE_PROVIDER_PUB_KEY` and `SERVICE_PROVIDER_CERT_PATH` to
|
|
79
|
+
the path to the certificate in PEM format. Self-signed or untrusted
|
|
80
|
+
certificates are supported.
|
|
81
|
+
- OR configure your application to use the certificate and private key
|
|
82
|
+
from `static/certs/(server.crt|key.pem)`.
|
|
15
83
|
|
|
16
|
-
|
|
84
|
+
MockPass accepts any value for `client_id`, `redirect_uri` and `sp_esvcId`.
|
|
85
|
+
The `client_secret` value will be checked if configured, see below.
|
|
86
|
+
|
|
87
|
+
Only the profiles (NRICs) that have entries in Mockpass' static dataset will
|
|
88
|
+
succeed, using other NRICs will result in an error. See the list of personas in
|
|
89
|
+
[static/myinfo/v3.json](static/myinfo/v3.json).
|
|
90
|
+
|
|
91
|
+
| Configuration item | Explanation |
|
|
92
|
+
|---|---|
|
|
93
|
+
| Client certificate | **Overview:** When client makes any request, what certificate is used to verify the request signature, and what certificate is used to encrypt the data payload. <br> **Default:** static certificate/key `static/certs/(server.crt|key.pub)` are used. <br> **How to configure:** Set the env var `SERVICE_PROVIDER_PUB_KEY` to the path to a public key PEM file, and `SERVICE_PROVIDER_CERT_PATH` to the path to a certificate PEM file. (A certificate PEM file can also be provided to `SERVICE_PROVIDER_PUB_KEY`, despite the env var name.) |
|
|
94
|
+
| Client secret | **Overview:** When client makes a Token request, whether MockPass verifies the request signature. <br> **Default:** Disabled. <br> **How to configure:** Enable for all requests by setting the env var `SERVICE_PROVIDER_MYINFO_SECRET` to some non-blank string. Provide this value to your application as well. |
|
|
95
|
+
| Payload encryption | **Overview:** When client makes a Person or Person-Basic request, whether MockPass encrypts the data payload. When client makes a Person request, whether MockPass verifies the request signature. <br> **Default:** Disabled. <br> **How to configure:** Enable for all requests by setting the env var `ENCRYPT_MYINFO` to `true`. |
|
|
96
|
+
|
|
97
|
+
To emulate the equivalent of the Test environment on Myinfo v3, you must both
|
|
98
|
+
set a client secret and enable payload encryption on MockPass.
|
|
99
|
+
|
|
100
|
+
### sgID v2
|
|
17
101
|
|
|
18
102
|
Configure your application to point to the following endpoints:
|
|
19
103
|
|
|
20
|
-
|
|
104
|
+
- http://localhost:5156/v2/.well-known/openid-configuration
|
|
105
|
+
- http://localhost:5156/v2/.well-known/jwks.json
|
|
106
|
+
- http://localhost:5156/v2/oauth/authorize
|
|
107
|
+
- http://localhost:5156/v2/oauth/token
|
|
108
|
+
- http://localhost:5156/v2/oauth/userinfo
|
|
109
|
+
|
|
110
|
+
Configure your application (or MockPass) with certificates/keys:
|
|
111
|
+
|
|
112
|
+
- Provide your application with the certificate `static/certs/spcp.crt` as the
|
|
113
|
+
sgID public key, or use the signing key published at the JWKS endpoint.
|
|
114
|
+
- EITHER configure MockPass with your application's X.509 certificate using the
|
|
115
|
+
env var `SERVICE_PROVIDER_PUB_KEY`, as the path to the certificate in PEM
|
|
116
|
+
format. Self-signed or untrusted certificates are supported.
|
|
117
|
+
- OR configure your application to use the certificate and private key
|
|
118
|
+
from `static/certs/(server.crt|key.pem)`.
|
|
119
|
+
|
|
120
|
+
MockPass accepts any value for `client_id`, `client_secret` and `redirect_uri`.
|
|
121
|
+
|
|
122
|
+
Only the profiles (NRICs) that have entries in Mockpass' static dataset will
|
|
123
|
+
succeed, using other NRICs will result in an error. See the list of personas in
|
|
124
|
+
[static/myinfo/v3.json](static/myinfo/v3.json).
|
|
125
|
+
|
|
126
|
+
| Configuration item | Explanation |
|
|
127
|
+
|---|---|
|
|
128
|
+
| Client certificate | **Overview:** When client makes any request, what certificate is used to verify the request signature, and what certificate is used to encrypt the data payload. <br> **Default:** static key `static/certs/key.pub` is used. <br> **How to configure:** Set the env var `SERVICE_PROVIDER_PUB_KEY` to the path to a public key PEM file. (A certificate PEM file can also be provided, despite the env var name.) |
|
|
129
|
+
| Login page | **Overview:** When client makes an authorize request, whether MockPass sends the client to a login page, instead of completing the login silently. <br> **Default:** Disabled for all requests. <br> **How to configure:** Enable for all requests by default by setting the env var `SHOW_LOGIN_PAGE` to `true`. Regardless of the default, you can override on a per-request basis by sending the HTTP request header `X-Show-Login-Page` with the value `true`. <br> **Detailed effect:** When login page is disabled, MockPass will immediately complete login and redirect to the `redirect_uri`. The profile used will be (in order of decreasing precedence) the profile with the NRIC specified in the env var `MOCKPASS_NRIC`, or the first profile in MockPass' static data. <br> When login page is enabled, MockPass returns a HTML page with a form that is used to complete the login. The client may select an existing profile, or provide a custom NRIC and UUID on the form. |
|
|
130
|
+
|
|
131
|
+
### Singpass/Corppass v1 (legacy)
|
|
132
|
+
|
|
133
|
+
**The v1 APIs should no longer be in use, see the v2 APIs above!**
|
|
134
|
+
|
|
135
|
+
Configure your application to point to the following endpoints:
|
|
136
|
+
|
|
137
|
+
Singpass (v1 - Singpass OIDC):
|
|
21
138
|
- http://localhost:5156/singpass/authorize - OIDC login redirect with optional page
|
|
22
139
|
- http://localhost:5156/singpass/token - receives OIDC authorization code and returns id_token
|
|
23
140
|
|
|
24
|
-
|
|
25
|
-
- http://localhost:5156/singpass/v2/authorize - OIDC login redirect with optional page
|
|
26
|
-
- http://localhost:5156/singpass/v2/token - receives OIDC authorization code and returns id_token
|
|
27
|
-
- http://localhost:5156/singpass/v2/.well-known/openid-configuration - OpenID discovery endpoint
|
|
28
|
-
- http://localhost:5156/singpass/v2/.well-known/keys - JWKS endpoint which exposes the auth provider's signing keys
|
|
29
|
-
|
|
30
|
-
CorpPass (v1 - Corppass OIDC):
|
|
141
|
+
Corppass (v1 - Corppass OIDC):
|
|
31
142
|
- http://localhost:5156/corppass/authorize - OIDC login redirect with optional page
|
|
32
143
|
- http://localhost:5156/corppass/token - receives OIDC authorization code and returns id_token
|
|
33
144
|
|
|
34
|
-
|
|
35
|
-
- http://localhost:5156/corppass/v2/authorize - OIDC login redirect with optional page
|
|
36
|
-
- http://localhost:5156/corppass/v2/token - receives OIDC authorization code and returns id_token
|
|
37
|
-
- http://localhost:5156/corppass/v2/.well-known/openid-configuration - OpenID discovery endpoint
|
|
38
|
-
- http://localhost:5156/corppass/v2/.well-known/keys - JWKS endpoint which exposes the auth provider's signing keys
|
|
145
|
+
Configure your application with keys and/or certificates:
|
|
39
146
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
147
|
+
Provide your application with the certificate `static/certs/spcp.crt` as the
|
|
148
|
+
Singpass/Corppass public certificate.
|
|
149
|
+
Provide the path to your application's X.509 certificate in PEM
|
|
150
|
+
format as env var `SERVICE_PROVIDER_CERT_PATH` when running MockPass.
|
|
151
|
+
Self-signed or untrusted certificates are supported.
|
|
152
|
+
Alternatively, provide your application with the certificate and private key
|
|
153
|
+
from `static/certs/(server.crt|key.pem)`.
|
|
45
154
|
|
|
46
|
-
|
|
47
|
-
- http://localhost:5156/v2/oauth/authorize
|
|
48
|
-
- http://localhost:5156/v2/oauth/token
|
|
49
|
-
- http://localhost:5156/v2/oauth/userinfo
|
|
50
|
-
- http://localhost:5156/v2/.well-known/openid-configuration - OpenID discovery endpoint
|
|
51
|
-
- http://localhost:5156/v2/.well-known/jwks.json - JWKS endpoint which exposes the auth provider's signing keys
|
|
155
|
+
### Run MockPass
|
|
52
156
|
|
|
53
|
-
|
|
54
|
-
and with application certs at `static/certs/{key.pem|server.crt}`
|
|
157
|
+
Common configuration:
|
|
55
158
|
|
|
56
|
-
|
|
57
|
-
|
|
159
|
+
| Configuration item | Explanation |
|
|
160
|
+
|---|---|
|
|
161
|
+
| Port number | **Overview:** What port number MockPass will listen for HTTP requests on. <br> **Default:** 5156. <br> **How to configure:** Set the env var `MOCKPASS_PORT` or `PORT` to some port number. |
|
|
162
|
+
| Stateless Mode | **Overview:** Enable for environments where the state of the process is not guaranteed, such as in serverless contexts. <br> **Default:** not set. <br> **How to configure:** Set the env var `MOCKPASS_STATELESS` to `true` or `false`. |
|
|
58
163
|
|
|
59
|
-
|
|
60
|
-
provide your well-known key endpoints as env vars `SP_RP_JWKS_ENDPOINT` and/or
|
|
61
|
-
`CP_RP_JWKS_ENDPOINT` respectively. Alternatively, provide your application with
|
|
62
|
-
the `oidc-v2-rp-*.json` JWKS.
|
|
164
|
+
Run MockPass:
|
|
63
165
|
|
|
64
166
|
```
|
|
65
167
|
$ npm install @opengovsg/mockpass
|
|
66
168
|
|
|
67
|
-
#
|
|
169
|
+
# Configure the listening port if desired, defaults to 5156
|
|
68
170
|
$ export MOCKPASS_PORT=5156
|
|
69
171
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
# Can be overridden per request using `X-Custom-NRIC`/`X-Custom-UUID`/`X-Custom-UEN` HTTP headers
|
|
74
|
-
$ export MOCKPASS_NRIC=S8979373D # Optional, defaults to first profile
|
|
75
|
-
|
|
76
|
-
# Disable signing/encryption (Optional, by default `true`)
|
|
77
|
-
$ export SIGN_ASSERTION=false
|
|
78
|
-
$ export ENCRYPT_ASSERTION=false
|
|
79
|
-
$ export SIGN_RESPONSE=false
|
|
80
|
-
$ export RESOLVE_ARTIFACT_REQUEST_SIGNED=false
|
|
81
|
-
|
|
82
|
-
# Encrypt payloads returned by /myinfo/v3/{person, person-basic},
|
|
83
|
-
# equivalent to MyInfo Auth Level L2 (testing and production)
|
|
84
|
-
$ export ENCRYPT_MYINFO=false
|
|
85
|
-
|
|
86
|
-
# If specified, will verify token provided in Authorization header
|
|
87
|
-
# for requests to /myinfo/*/token
|
|
172
|
+
# Configure any other options if required
|
|
173
|
+
$ export SHOW_LOGIN_PAGE=false
|
|
174
|
+
$ export MOCKPASS_NRIC=S8979373D
|
|
88
175
|
$ export SERVICE_PROVIDER_MYINFO_SECRET=<your secret here>
|
|
176
|
+
$ export ENCRYPT_MYINFO=false
|
|
89
177
|
|
|
90
178
|
$ npx mockpass
|
|
91
179
|
MockPass listening on 5156
|
|
92
|
-
```
|
|
93
180
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
with SingPass/CorpPass using a dev machine alone. This is awkward for developers
|
|
98
|
-
who then need to connect to the staging servers hosted by SingPass/CorpPass,
|
|
99
|
-
which may not always be available (eg, down for maintenance, or no Internet).
|
|
100
|
-
|
|
101
|
-
MockPass tries to solves this by providing an extremely lightweight implementation
|
|
102
|
-
of an OIDC Provider that returns mock SingPass and CorpPass assertions.
|
|
103
|
-
It optionally provides a mock login page that (badly) mimics the SingPass/CorpPass
|
|
104
|
-
login experience.
|
|
181
|
+
# Alternatively, just run directly with npx
|
|
182
|
+
MOCKPASS_PORT=5156 SHOW_LOGIN_PAGE=false MOCKPASS_NRIC=S8979373D npx @opengovsg/mockpass@latest
|
|
183
|
+
```
|
|
105
184
|
|
|
106
185
|
## Contributing
|
|
107
186
|
|
package/app.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
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
|
package/index.js
CHANGED
|
@@ -1,63 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
const
|
|
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')
|
|
2
|
+
const { app } = require('./app')
|
|
14
3
|
|
|
15
4
|
const PORT = process.env.MOCKPASS_PORT || process.env.PORT || 5156
|
|
16
5
|
|
|
17
|
-
const serviceProvider = {
|
|
18
|
-
cert: fs.readFileSync(
|
|
19
|
-
path.resolve(
|
|
20
|
-
__dirname,
|
|
21
|
-
process.env.SERVICE_PROVIDER_CERT_PATH || './static/certs/server.crt',
|
|
22
|
-
),
|
|
23
|
-
),
|
|
24
|
-
pubKey: fs.readFileSync(
|
|
25
|
-
path.resolve(
|
|
26
|
-
__dirname,
|
|
27
|
-
process.env.SERVICE_PROVIDER_PUB_KEY || './static/certs/key.pub',
|
|
28
|
-
),
|
|
29
|
-
),
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const cryptoConfig = {
|
|
33
|
-
signAssertion: process.env.SIGN_ASSERTION !== 'false', // default to true to be backward compatable
|
|
34
|
-
signResponse: process.env.SIGN_RESPONSE !== 'false',
|
|
35
|
-
encryptAssertion: process.env.ENCRYPT_ASSERTION !== 'false',
|
|
36
|
-
resolveArtifactRequestSigned:
|
|
37
|
-
process.env.RESOLVE_ARTIFACT_REQUEST_SIGNED !== 'false',
|
|
38
|
-
}
|
|
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
|
-
}
|
|
47
|
-
|
|
48
|
-
const app = express()
|
|
49
|
-
app.use(morgan('combined'))
|
|
50
|
-
|
|
51
|
-
configOIDC(app, options)
|
|
52
|
-
configOIDCv2(app, options)
|
|
53
|
-
configSGID(app, options)
|
|
54
|
-
|
|
55
|
-
configMyInfo.consent(app)
|
|
56
|
-
configMyInfo.v3(app, options)
|
|
57
|
-
|
|
58
|
-
app.enable('trust proxy')
|
|
59
|
-
app.use(express.static(path.join(__dirname, 'public')))
|
|
60
|
-
|
|
61
6
|
app.listen(PORT, (err) =>
|
|
62
7
|
err
|
|
63
8
|
? console.error('Unable to start MockPass', err)
|
package/lib/auth-code.js
CHANGED
|
@@ -4,14 +4,24 @@ const crypto = require('crypto')
|
|
|
4
4
|
const AUTH_CODE_TIMEOUT = 5 * 60 * 1000
|
|
5
5
|
const profileAndNonceStore = new ExpiryMap(AUTH_CODE_TIMEOUT)
|
|
6
6
|
|
|
7
|
-
const generateAuthCode = (
|
|
8
|
-
|
|
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
|
+
|
|
9
17
|
profileAndNonceStore.set(authCode, { profile, scopes, nonce })
|
|
10
18
|
return authCode
|
|
11
19
|
}
|
|
12
20
|
|
|
13
|
-
const lookUpByAuthCode = (authCode) => {
|
|
14
|
-
return
|
|
21
|
+
const lookUpByAuthCode = (authCode, { isStateless = false }) => {
|
|
22
|
+
return isStateless
|
|
23
|
+
? JSON.parse(Buffer.from(authCode, 'base64url').toString('utf-8'))
|
|
24
|
+
: profileAndNonceStore.get(authCode)
|
|
15
25
|
}
|
|
16
26
|
|
|
17
27
|
module.exports = { generateAuthCode, lookUpByAuthCode }
|
|
@@ -47,13 +47,13 @@ const authorizeViaOIDC = authorize(
|
|
|
47
47
|
`/singpass/authorize?client_id=MYINFO-CONSENTPLATFORM&redirect_uri=${MYINFO_ASSERT_ENDPOINT}&state=${state}`,
|
|
48
48
|
)
|
|
49
49
|
|
|
50
|
-
function config(app) {
|
|
50
|
+
function config(app, { isStateless }) {
|
|
51
51
|
app.get(MYINFO_ASSERT_ENDPOINT, (req, res) => {
|
|
52
52
|
const rawArtifact = req.query.SAMLart || req.query.code
|
|
53
53
|
const artifact = rawArtifact.replace(/ /g, '+')
|
|
54
54
|
const state = req.query.RelayState || req.query.state
|
|
55
55
|
|
|
56
|
-
const profile = lookUpByAuthCode(artifact).profile
|
|
56
|
+
const profile = lookUpByAuthCode(artifact, { isStateless }).profile
|
|
57
57
|
const myinfoVersion = 'v3'
|
|
58
58
|
|
|
59
59
|
const { nric: id } = profile
|
package/lib/express/oidc/spcp.js
CHANGED
|
@@ -24,7 +24,7 @@ const signingPem = fs.readFileSync(
|
|
|
24
24
|
path.resolve(__dirname, '../../../static/certs/spcp-key.pem'),
|
|
25
25
|
)
|
|
26
26
|
|
|
27
|
-
function config(app, { showLoginPage, serviceProvider }) {
|
|
27
|
+
function config(app, { showLoginPage, serviceProvider, isStateless }) {
|
|
28
28
|
for (const idp of ['singPass', 'corpPass']) {
|
|
29
29
|
const profiles = assertions.oidc[idp]
|
|
30
30
|
const defaultProfile =
|
|
@@ -34,7 +34,7 @@ function config(app, { showLoginPage, serviceProvider }) {
|
|
|
34
34
|
const { redirect_uri: redirectURI, state, nonce } = req.query
|
|
35
35
|
if (showLoginPage(req)) {
|
|
36
36
|
const values = profiles.map((profile) => {
|
|
37
|
-
const authCode = generateAuthCode({ profile, nonce })
|
|
37
|
+
const authCode = generateAuthCode({ profile, nonce }, { isStateless })
|
|
38
38
|
const assertURL = buildAssertURL(redirectURI, authCode, state)
|
|
39
39
|
const id = idGenerator[idp](profile)
|
|
40
40
|
return { id, assertURL }
|
|
@@ -53,7 +53,7 @@ function config(app, { showLoginPage, serviceProvider }) {
|
|
|
53
53
|
res.send(response)
|
|
54
54
|
} else {
|
|
55
55
|
const profile = customProfileFromHeaders[idp](req) || defaultProfile
|
|
56
|
-
const authCode = generateAuthCode({ profile, nonce })
|
|
56
|
+
const authCode = generateAuthCode({ profile, nonce }, { isStateless })
|
|
57
57
|
const assertURL = buildAssertURL(redirectURI, authCode, state)
|
|
58
58
|
console.warn(
|
|
59
59
|
`Redirecting login from ${req.query.client_id} to ${redirectURI}`,
|
|
@@ -72,7 +72,7 @@ function config(app, { showLoginPage, serviceProvider }) {
|
|
|
72
72
|
profile.uen = uen
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
const authCode = generateAuthCode({ profile, nonce })
|
|
75
|
+
const authCode = generateAuthCode({ profile, nonce }, { isStateless })
|
|
76
76
|
const assertURL = buildAssertURL(redirectURI, authCode, state)
|
|
77
77
|
res.redirect(assertURL)
|
|
78
78
|
})
|
|
@@ -88,20 +88,32 @@ function config(app, { showLoginPage, serviceProvider }) {
|
|
|
88
88
|
const { refresh_token: suppliedRefreshToken } = req.body
|
|
89
89
|
console.warn(`Refreshing tokens with ${suppliedRefreshToken}`)
|
|
90
90
|
|
|
91
|
-
profile =
|
|
91
|
+
profile = isStateless
|
|
92
|
+
? JSON.parse(
|
|
93
|
+
Buffer.from(suppliedRefreshToken, 'base64url').toString(
|
|
94
|
+
'utf-8',
|
|
95
|
+
),
|
|
96
|
+
)
|
|
97
|
+
: profileStore.get(suppliedRefreshToken)
|
|
92
98
|
} else {
|
|
93
99
|
const { code: authCode } = req.body
|
|
94
100
|
console.warn(
|
|
95
101
|
`Received auth code ${authCode} from ${aud} and ${req.body.redirect_uri}`,
|
|
96
102
|
)
|
|
97
|
-
;({ profile, nonce } = lookUpByAuthCode(authCode))
|
|
103
|
+
;({ profile, nonce } = lookUpByAuthCode(authCode, { isStateless }))
|
|
98
104
|
}
|
|
99
105
|
|
|
100
106
|
const iss = `${req.protocol}://${req.get('host')}`
|
|
101
107
|
|
|
102
|
-
const {
|
|
103
|
-
|
|
108
|
+
const {
|
|
109
|
+
idTokenClaims,
|
|
110
|
+
accessToken,
|
|
111
|
+
refreshToken: generatedRefreshToken,
|
|
112
|
+
} = await assertions.oidc.create[idp](profile, iss, aud, nonce)
|
|
104
113
|
|
|
114
|
+
const refreshToken = isStateless
|
|
115
|
+
? Buffer.from(JSON.stringify(profile)).toString('base64url')
|
|
116
|
+
: generatedRefreshToken
|
|
105
117
|
profileStore.set(refreshToken, profile)
|
|
106
118
|
|
|
107
119
|
const signingKey = await jose.JWK.asKey(signingPem, 'pem')
|
|
@@ -140,7 +140,7 @@ function findEncryptionKey(jwks, algs) {
|
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
function config(app, { showLoginPage }) {
|
|
143
|
+
function config(app, { showLoginPage, isStateless }) {
|
|
144
144
|
for (const idp of ['singPass', 'corpPass']) {
|
|
145
145
|
const profiles = assertions.oidc[idp]
|
|
146
146
|
const defaultProfile =
|
|
@@ -196,7 +196,7 @@ function config(app, { showLoginPage }) {
|
|
|
196
196
|
// Identical to OIDC v1
|
|
197
197
|
if (showLoginPage(req)) {
|
|
198
198
|
const values = profiles.map((profile) => {
|
|
199
|
-
const authCode = generateAuthCode({ profile, nonce })
|
|
199
|
+
const authCode = generateAuthCode({ profile, nonce }, { isStateless })
|
|
200
200
|
const assertURL = buildAssertURL(redirectURI, authCode, state)
|
|
201
201
|
const id = idGenerator[idp](profile)
|
|
202
202
|
return { id, assertURL }
|
|
@@ -215,7 +215,7 @@ function config(app, { showLoginPage }) {
|
|
|
215
215
|
res.send(response)
|
|
216
216
|
} else {
|
|
217
217
|
const profile = customProfileFromHeaders[idp](req) || defaultProfile
|
|
218
|
-
const authCode = generateAuthCode({ profile, nonce })
|
|
218
|
+
const authCode = generateAuthCode({ profile, nonce }, { isStateless })
|
|
219
219
|
const assertURL = buildAssertURL(redirectURI, authCode, state)
|
|
220
220
|
console.warn(
|
|
221
221
|
`Redirecting login from ${req.query.client_id} to ${redirectURI}`,
|
|
@@ -234,7 +234,7 @@ function config(app, { showLoginPage }) {
|
|
|
234
234
|
profile.uen = uen
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
-
const authCode = generateAuthCode({ profile, nonce })
|
|
237
|
+
const authCode = generateAuthCode({ profile, nonce }, { isStateless })
|
|
238
238
|
const assertURL = buildAssertURL(redirectURI, authCode, state)
|
|
239
239
|
res.redirect(assertURL)
|
|
240
240
|
})
|
|
@@ -434,7 +434,7 @@ function config(app, { showLoginPage }) {
|
|
|
434
434
|
}
|
|
435
435
|
|
|
436
436
|
// Step 1: Obtain profile for which the auth code requested data for
|
|
437
|
-
const { profile, nonce } = lookUpByAuthCode(authCode)
|
|
437
|
+
const { profile, nonce } = lookUpByAuthCode(authCode, { isStateless })
|
|
438
438
|
|
|
439
439
|
// Step 2: Get ID token
|
|
440
440
|
const aud = clientAssertionClaims['sub']
|
package/lib/express/sgid.js
CHANGED
|
@@ -30,7 +30,7 @@ const buildAssertURL = (redirectURI, authCode, state) =>
|
|
|
30
30
|
authCode,
|
|
31
31
|
)}&state=${encodeURIComponent(state)}`
|
|
32
32
|
|
|
33
|
-
function config(app, { showLoginPage, serviceProvider }) {
|
|
33
|
+
function config(app, { showLoginPage, serviceProvider, isStateless }) {
|
|
34
34
|
const profiles = assertions.oidc.singPass
|
|
35
35
|
const defaultProfile =
|
|
36
36
|
profiles.find((p) => p.nric === process.env.MOCKPASS_NRIC) || profiles[0]
|
|
@@ -43,7 +43,10 @@ function config(app, { showLoginPage, serviceProvider }) {
|
|
|
43
43
|
const values = profiles
|
|
44
44
|
.filter((profile) => assertions.myinfo.v3.personas[profile.nric])
|
|
45
45
|
.map((profile) => {
|
|
46
|
-
const authCode = generateAuthCode(
|
|
46
|
+
const authCode = generateAuthCode(
|
|
47
|
+
{ profile, scopes, nonce },
|
|
48
|
+
{ isStateless },
|
|
49
|
+
)
|
|
47
50
|
const assertURL = buildAssertURL(redirectURI, authCode, state)
|
|
48
51
|
const id = idGenerator.singPass(profile)
|
|
49
52
|
return { id, assertURL }
|
|
@@ -52,7 +55,10 @@ function config(app, { showLoginPage, serviceProvider }) {
|
|
|
52
55
|
res.send(response)
|
|
53
56
|
} else {
|
|
54
57
|
const profile = defaultProfile
|
|
55
|
-
const authCode = generateAuthCode(
|
|
58
|
+
const authCode = generateAuthCode(
|
|
59
|
+
{ profile, scopes, nonce },
|
|
60
|
+
{ isStateless },
|
|
61
|
+
)
|
|
56
62
|
const assertURL = buildAssertURL(redirectURI, authCode, state)
|
|
57
63
|
console.info(
|
|
58
64
|
`Redirecting login from ${req.query.client_id} to ${assertURL}`,
|
|
@@ -74,7 +80,9 @@ function config(app, { showLoginPage, serviceProvider }) {
|
|
|
74
80
|
)
|
|
75
81
|
|
|
76
82
|
try {
|
|
77
|
-
const { profile, scopes, nonce } = lookUpByAuthCode(authCode
|
|
83
|
+
const { profile, scopes, nonce } = lookUpByAuthCode(authCode, {
|
|
84
|
+
isStateless,
|
|
85
|
+
})
|
|
78
86
|
console.info(
|
|
79
87
|
`Profile ${JSON.stringify(profile)} with token scope ${scopes}`,
|
|
80
88
|
)
|
|
@@ -120,7 +128,9 @@ function config(app, { showLoginPage, serviceProvider }) {
|
|
|
120
128
|
req.headers.authorization || req.headers.Authorization
|
|
121
129
|
).replace('Bearer ', '')
|
|
122
130
|
// eslint-disable-next-line no-unused-vars
|
|
123
|
-
const { profile, scopes, unused } = lookUpByAuthCode(authCode
|
|
131
|
+
const { profile, scopes, unused } = lookUpByAuthCode(authCode, {
|
|
132
|
+
isStateless,
|
|
133
|
+
})
|
|
124
134
|
const uuid = profile.uuid
|
|
125
135
|
const nric = assertions.oidc.singPass.find((p) => p.uuid === uuid).nric
|
|
126
136
|
const persona = assertions.myinfo.v3.personas[nric]
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengovsg/mockpass",
|
|
3
|
-
"version": "4.0
|
|
3
|
+
"version": "4.1.0",
|
|
4
4
|
"description": "A mock SingPass/CorpPass server for dev purposes",
|
|
5
|
-
"main": "
|
|
5
|
+
"main": "app.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"mockpass": "index.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"test": "echo \"Error: no test specified\" && exit 1",
|
|
11
|
-
"start": "nodemon",
|
|
11
|
+
"start": "nodemon index",
|
|
12
12
|
"cz": "git-cz",
|
|
13
13
|
"lint": "eslint lib",
|
|
14
14
|
"lint-fix": "eslint --fix lib",
|