adapt-authoring-auth 0.0.1
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/.eslintignore +1 -0
- package/.eslintrc +14 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +55 -0
- package/.github/ISSUE_TEMPLATE/config.yml +1 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +22 -0
- package/.github/dependabot.yml +11 -0
- package/.github/pull_request_template.md +25 -0
- package/.github/workflows/labelled_prs.yml +16 -0
- package/.github/workflows/new.yml +19 -0
- package/adapt-authoring.json +5 -0
- package/conf/config.schema.json +40 -0
- package/errors/errors.json +77 -0
- package/index.js +8 -0
- package/lib/AbstractAuthModule.js +208 -0
- package/lib/AuthModule.js +127 -0
- package/lib/AuthToken.js +175 -0
- package/lib/AuthUtils.js +65 -0
- package/lib/Authentication.js +195 -0
- package/lib/Permissions.js +111 -0
- package/lib/apidefs.js +126 -0
- package/package.json +24 -0
- package/schema/authtoken.schema.json +39 -0
- package/schema/authuser.schema.json +17 -0
package/.eslintignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
node_modules
|
package/.eslintrc
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
name: Bug Report
|
|
2
|
+
description: File a bug report
|
|
3
|
+
labels: ["bug"]
|
|
4
|
+
body:
|
|
5
|
+
- type: markdown
|
|
6
|
+
attributes:
|
|
7
|
+
value: |
|
|
8
|
+
Thanks for taking the time to fill out this bug report!
|
|
9
|
+
- type: textarea
|
|
10
|
+
id: description
|
|
11
|
+
attributes:
|
|
12
|
+
label: What happened?
|
|
13
|
+
description: Please describe the issue
|
|
14
|
+
validations:
|
|
15
|
+
required: true
|
|
16
|
+
- type: textarea
|
|
17
|
+
id: expected
|
|
18
|
+
attributes:
|
|
19
|
+
label: Expected behaviour
|
|
20
|
+
description: Tell us what should have happened
|
|
21
|
+
- type: textarea
|
|
22
|
+
id: repro-steps
|
|
23
|
+
attributes:
|
|
24
|
+
label: Steps to reproduce
|
|
25
|
+
description: Tell us how to reproduce the issue
|
|
26
|
+
validations:
|
|
27
|
+
required: true
|
|
28
|
+
- type: input
|
|
29
|
+
id: aat-version
|
|
30
|
+
attributes:
|
|
31
|
+
label: Authoring tool version
|
|
32
|
+
description: What version of the Adapt authoring tool are you running?
|
|
33
|
+
validations:
|
|
34
|
+
required: true
|
|
35
|
+
- type: input
|
|
36
|
+
id: fw-version
|
|
37
|
+
attributes:
|
|
38
|
+
label: Framework version
|
|
39
|
+
description: What version of the Adapt framework are you running?
|
|
40
|
+
- type: dropdown
|
|
41
|
+
id: browsers
|
|
42
|
+
attributes:
|
|
43
|
+
label: What browsers are you seeing the problem on?
|
|
44
|
+
multiple: true
|
|
45
|
+
options:
|
|
46
|
+
- Firefox
|
|
47
|
+
- Chrome
|
|
48
|
+
- Safari
|
|
49
|
+
- Microsoft Edge
|
|
50
|
+
- type: textarea
|
|
51
|
+
id: logs
|
|
52
|
+
attributes:
|
|
53
|
+
label: Relevant log output
|
|
54
|
+
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
|
|
55
|
+
render: sh
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
blank_issues_enabled: false
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: Feature request
|
|
2
|
+
description: Request a new feature
|
|
3
|
+
labels: ["enhancement"]
|
|
4
|
+
body:
|
|
5
|
+
- type: markdown
|
|
6
|
+
attributes:
|
|
7
|
+
value: |
|
|
8
|
+
Thanks for taking the time to request a new feature in the Adapt authoring tool! The Adapt team will consider all new feature requests, but unfortunately cannot commit to every one.
|
|
9
|
+
- type: textarea
|
|
10
|
+
id: description
|
|
11
|
+
attributes:
|
|
12
|
+
label: Feature description
|
|
13
|
+
description: Please describe your feature request
|
|
14
|
+
validations:
|
|
15
|
+
required: true
|
|
16
|
+
- type: checkboxes
|
|
17
|
+
id: contribute
|
|
18
|
+
attributes:
|
|
19
|
+
label: Can you work on this feature?
|
|
20
|
+
description: If you are able to commit your own time to work on this feature, it will greatly increase the liklihood of it being implemented by the core dev team. Otherwise, it will be triaged and prioritised alongside the core team's current priorities.
|
|
21
|
+
options:
|
|
22
|
+
- label: I can contribute
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# To get started with Dependabot version updates, you'll need to specify which
|
|
2
|
+
# package ecosystems to update and where the package manifests are located.
|
|
3
|
+
# Please see the documentation for all configuration options:
|
|
4
|
+
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
|
5
|
+
|
|
6
|
+
version: 2
|
|
7
|
+
updates:
|
|
8
|
+
- package-ecosystem: "npm" # See documentation for possible values
|
|
9
|
+
directory: "/" # Location of package manifests
|
|
10
|
+
schedule:
|
|
11
|
+
interval: "weekly"
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[//]: # (Please title your PR according to eslint commit conventions)
|
|
2
|
+
[//]: # (See https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-eslint#eslint-convention for details)
|
|
3
|
+
|
|
4
|
+
[//]: # (Add a link to the original issue)
|
|
5
|
+
|
|
6
|
+
[//]: # (Delete as appropriate)
|
|
7
|
+
### Fix
|
|
8
|
+
* A sentence describing each fix
|
|
9
|
+
|
|
10
|
+
### Update
|
|
11
|
+
* A sentence describing each udpate
|
|
12
|
+
|
|
13
|
+
### New
|
|
14
|
+
* A sentence describing each new feature
|
|
15
|
+
|
|
16
|
+
### Breaking
|
|
17
|
+
* A sentence describing each breaking change
|
|
18
|
+
|
|
19
|
+
[//]: # (List appropriate steps for testing if needed)
|
|
20
|
+
### Testing
|
|
21
|
+
1. Steps for testing
|
|
22
|
+
|
|
23
|
+
[//]: # (Mention any other dependencies)
|
|
24
|
+
|
|
25
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
name: Add labelled PRs to project
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
types: [ labeled ]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
add-to-project:
|
|
9
|
+
if: ${{ github.event.label.name == 'dependencies' }}
|
|
10
|
+
name: Add to main project
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/add-to-project@v0.1.0
|
|
14
|
+
with:
|
|
15
|
+
project-url: https://github.com/orgs/adapt-security/projects/5
|
|
16
|
+
github-token: ${{ secrets.PROJECTS_SECRET }}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
name: Add to main project
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
issues:
|
|
5
|
+
types:
|
|
6
|
+
- opened
|
|
7
|
+
pull_request:
|
|
8
|
+
types:
|
|
9
|
+
- opened
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
add-to-project:
|
|
13
|
+
name: Add to main project
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/add-to-project@v0.1.0
|
|
17
|
+
with:
|
|
18
|
+
project-url: https://github.com/orgs/adapt-security/projects/5
|
|
19
|
+
github-token: ${{ secrets.PROJECTS_SECRET }}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"properties": {
|
|
5
|
+
"isEnabled": {
|
|
6
|
+
"description": "Enables auth (note: this has no effect in production environments)",
|
|
7
|
+
"type": "boolean",
|
|
8
|
+
"default": true
|
|
9
|
+
},
|
|
10
|
+
"defaultTokenLifespan": {
|
|
11
|
+
"description": "How long a token should remain valid for",
|
|
12
|
+
"type": "string",
|
|
13
|
+
"default": "7d"
|
|
14
|
+
},
|
|
15
|
+
"logMissingPermissions": {
|
|
16
|
+
"description": "In enabled, a warning is logged on app start for any routes with missing permissions (note: any routes without defined permissions are inaccessible)",
|
|
17
|
+
"type": "boolean",
|
|
18
|
+
"default": true
|
|
19
|
+
},
|
|
20
|
+
"secureTokenGeneration": {
|
|
21
|
+
"description": "Whether a user must be authenticated to generate an API auth token",
|
|
22
|
+
"type": "boolean",
|
|
23
|
+
"default": true
|
|
24
|
+
},
|
|
25
|
+
"tokenSecret": {
|
|
26
|
+
"description": "A secret used to encode/decode Json Web Tokens",
|
|
27
|
+
"type": "string",
|
|
28
|
+
"minLength": 10,
|
|
29
|
+
"_adapt": {
|
|
30
|
+
"isSecret": true
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"tokenIssuer": {
|
|
34
|
+
"description": "The identity of the issuer of the token",
|
|
35
|
+
"type": "string",
|
|
36
|
+
"default": "adapt"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"required": ["tokenSecret"]
|
|
40
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"ACCOUNT_DISABLED": {
|
|
3
|
+
"description": "Account has been disabled",
|
|
4
|
+
"statusCode": 403
|
|
5
|
+
},
|
|
6
|
+
"AUTH_HEADER_UNSUPPORTED": {
|
|
7
|
+
"data": {
|
|
8
|
+
"type": "Authorisation type provided"
|
|
9
|
+
},
|
|
10
|
+
"description": "Authorization header type is unsupported by the API",
|
|
11
|
+
"statusCode": 401
|
|
12
|
+
},
|
|
13
|
+
"AUTH_PLUGIN_INVALID_CLASS": {
|
|
14
|
+
"description": "Auth plugin must extend AbstractAuthPlugin",
|
|
15
|
+
"statusCode": 500
|
|
16
|
+
},
|
|
17
|
+
"AUTH_TOKEN_EXPIRED": {
|
|
18
|
+
"description": "Auth token has expired, a new one must be created",
|
|
19
|
+
"statusCode": 401
|
|
20
|
+
},
|
|
21
|
+
"AUTH_TOKEN_INVALID": {
|
|
22
|
+
"data": {
|
|
23
|
+
"error": "The error message"
|
|
24
|
+
},
|
|
25
|
+
"description": "Auth token is not valid",
|
|
26
|
+
"statusCode": 401
|
|
27
|
+
},
|
|
28
|
+
"AUTH_TOKEN_NOT_BEFORE": {
|
|
29
|
+
"data": {
|
|
30
|
+
"error": "The error message"
|
|
31
|
+
},
|
|
32
|
+
"description": "The auth token nbf is after the current time, and the token is therefore not valid",
|
|
33
|
+
"statusCode": 401
|
|
34
|
+
},
|
|
35
|
+
"AUTH_TYPE_DEF_MISSING": {
|
|
36
|
+
"description": "Auth type is not defined",
|
|
37
|
+
"statusCode": 500
|
|
38
|
+
},
|
|
39
|
+
"DUPL_AUTH_PLUGIN_REG": {
|
|
40
|
+
"description": "An auth plugin is already registered with the same name",
|
|
41
|
+
"statusCode": 500
|
|
42
|
+
},
|
|
43
|
+
"INVALID_LOGIN_DETAILS": {
|
|
44
|
+
"description": "Invalid login details were provided",
|
|
45
|
+
"statusCode": 401
|
|
46
|
+
},
|
|
47
|
+
"MISSING_AUTH_HEADER": {
|
|
48
|
+
"description": "Authorization headers are missing from request",
|
|
49
|
+
"statusCode": 401
|
|
50
|
+
},
|
|
51
|
+
"UNAUTHENTICATED": {
|
|
52
|
+
"description": "Request is not authenticated for access to the API",
|
|
53
|
+
"statusCode": 401
|
|
54
|
+
},
|
|
55
|
+
"UNAUTHORISED": {
|
|
56
|
+
"data": {
|
|
57
|
+
"method": "The request HTTP method",
|
|
58
|
+
"url": "The request URL"
|
|
59
|
+
},
|
|
60
|
+
"description": "Request is not authorised to perform the required operation",
|
|
61
|
+
"statusCode": 403
|
|
62
|
+
},
|
|
63
|
+
"UNKNOWN_AUTH_TYPE": {
|
|
64
|
+
"data": {
|
|
65
|
+
"authType": "Authentication type"
|
|
66
|
+
},
|
|
67
|
+
"description": "Request is attempting to use an unknown authentication type",
|
|
68
|
+
"statusCode": 401
|
|
69
|
+
},
|
|
70
|
+
"USER_REG_FAILED": {
|
|
71
|
+
"data": {
|
|
72
|
+
"error": "The error"
|
|
73
|
+
},
|
|
74
|
+
"description": "User registration failed",
|
|
75
|
+
"statusCode": 400
|
|
76
|
+
}
|
|
77
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentication
|
|
3
|
+
* @namespace auth
|
|
4
|
+
*/
|
|
5
|
+
export { default as AbstractAuthModule } from './lib/AbstractAuthModule.js'
|
|
6
|
+
export { default as AuthToken } from './lib/AuthToken.js'
|
|
7
|
+
export { default as AuthUtils } from './lib/AuthUtils.js'
|
|
8
|
+
export { default } from './lib/AuthModule.js'
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { AbstractModule, Hook } from 'adapt-authoring-core'
|
|
2
|
+
import apidefs from './apidefs.js'
|
|
3
|
+
import AuthToken from './AuthToken.js'
|
|
4
|
+
/**
|
|
5
|
+
* Abstract module to be overridden by specific auth implementations
|
|
6
|
+
* @memberof auth
|
|
7
|
+
* @extends {AbstractModule}
|
|
8
|
+
*/
|
|
9
|
+
class AbstractAuthModule extends AbstractModule {
|
|
10
|
+
/**
|
|
11
|
+
* Initialises the module
|
|
12
|
+
* @return {Promise}
|
|
13
|
+
*/
|
|
14
|
+
async init () {
|
|
15
|
+
await this.setValues()
|
|
16
|
+
if (!this.type) {
|
|
17
|
+
throw this.app.errors.AUTH_TYPE_DEF_MISSING
|
|
18
|
+
}
|
|
19
|
+
const [auth, users] = await this.app.waitForModule('auth', 'users')
|
|
20
|
+
/**
|
|
21
|
+
* Cached reference to the auth module
|
|
22
|
+
* @type {AuthModule}
|
|
23
|
+
*/
|
|
24
|
+
this.auth = auth
|
|
25
|
+
/**
|
|
26
|
+
* Cached reference to the auth module
|
|
27
|
+
* @type {UsersModule}
|
|
28
|
+
*/
|
|
29
|
+
this.users = users
|
|
30
|
+
/**
|
|
31
|
+
* The router instance
|
|
32
|
+
* @type {Router}
|
|
33
|
+
*/
|
|
34
|
+
this.router = this.auth.router.createChildRouter(this.type)
|
|
35
|
+
this.router.addRoute({
|
|
36
|
+
route: '/',
|
|
37
|
+
handlers: { post: this.authenticateHandler.bind(this) }
|
|
38
|
+
}, {
|
|
39
|
+
route: '/register',
|
|
40
|
+
handlers: { post: this.registerHandler.bind(this) }
|
|
41
|
+
}, {
|
|
42
|
+
route: '/enable',
|
|
43
|
+
handlers: { post: this.enableHandler.bind(this) },
|
|
44
|
+
meta: apidefs.enable
|
|
45
|
+
}, {
|
|
46
|
+
route: '/disable',
|
|
47
|
+
handlers: { post: this.enableHandler.bind(this) },
|
|
48
|
+
meta: apidefs.disable
|
|
49
|
+
}, ...(this.routes || []))
|
|
50
|
+
|
|
51
|
+
this.secureRoute('/register', 'post', ['register:users'])
|
|
52
|
+
this.secureRoute('/enable', 'post', ['write:users'])
|
|
53
|
+
this.secureRoute('/disable', 'post', ['write:users'])
|
|
54
|
+
this.unsecureRoute('/', 'post')
|
|
55
|
+
/**
|
|
56
|
+
* Hook which is invoked when a new user is registered in the system
|
|
57
|
+
* @type {Hook}
|
|
58
|
+
*/
|
|
59
|
+
this.registerHook = new Hook({ mutable: true })
|
|
60
|
+
|
|
61
|
+
this.auth.authentication.registerPlugin(this.type, this)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Sets initial module values (set during initialisation), can be called by subclasses
|
|
66
|
+
* @return {Promise}
|
|
67
|
+
*/
|
|
68
|
+
async setValues () {
|
|
69
|
+
/**
|
|
70
|
+
* Identifier for the auth type
|
|
71
|
+
* @type {String}
|
|
72
|
+
*/
|
|
73
|
+
this.type = undefined
|
|
74
|
+
/**
|
|
75
|
+
* Custom endpoints for the auth type
|
|
76
|
+
* @type {Array<Route>}
|
|
77
|
+
*/
|
|
78
|
+
this.routes = undefined
|
|
79
|
+
/**
|
|
80
|
+
* Name of the schema to use when validating a user using this auth type
|
|
81
|
+
* @type {String}
|
|
82
|
+
*/
|
|
83
|
+
this.userSchema = 'user'
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Locks a route to only users with the passed permissions scopes
|
|
88
|
+
* @param {String} route The route
|
|
89
|
+
* @param {String} method The HTTP method
|
|
90
|
+
* @param {Array<String>} scopes Permissions scopes
|
|
91
|
+
*/
|
|
92
|
+
secureRoute (route, method, scopes) {
|
|
93
|
+
this.auth.secureRoute(`${this.router.path}${route}`, method, scopes)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Removes auth checks from a single route {@link Auth#unsecureRoute}
|
|
98
|
+
* @param {String} route The route
|
|
99
|
+
* @param {String} method The HTTP method
|
|
100
|
+
*/
|
|
101
|
+
unsecureRoute (route, method) {
|
|
102
|
+
this.auth.unsecureRoute(`${this.router.path}${route}`, method)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Registers a new user
|
|
107
|
+
* @param {Object} data Data to be used for doc creation
|
|
108
|
+
* @return {Promise} Resolves with the new user's data
|
|
109
|
+
*/
|
|
110
|
+
async register (data) {
|
|
111
|
+
return this.auth.authentication.registerUser(this.type, data)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Sets the appropriate attributes to enable/disable user
|
|
116
|
+
* @param {Object} user User DB document
|
|
117
|
+
* @param {boolean} isEnabled Whether the user should be enabled
|
|
118
|
+
* @return {Promise}
|
|
119
|
+
*/
|
|
120
|
+
async setUserEnabled (user, isEnabled) {
|
|
121
|
+
await this.users.update({ _id: user._id }, { isEnabled })
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* A convenience function for accessing Authentication#disavowUser
|
|
126
|
+
* @param {object} query Search query
|
|
127
|
+
* @return {Promise}
|
|
128
|
+
*/
|
|
129
|
+
async disavowUser (query) {
|
|
130
|
+
return this.auth.authentication.disavowUser(query)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Checks whether a user is allowed access to the APIs and performs any related auth type specific actions
|
|
135
|
+
* @param {Object} user The user record
|
|
136
|
+
* @param {external:ExpressRequest} req
|
|
137
|
+
* @param {external:ExpressResponse} res
|
|
138
|
+
* @return {Promise} Resolves on success
|
|
139
|
+
*/
|
|
140
|
+
async authenticate (user, req, res) {
|
|
141
|
+
throw this.app.errors.FUNC_NOT_OVERRIDDEN.setData({ name: `${this.constructor.name}#authenticate` })
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Handles authentication requests
|
|
146
|
+
* @param {external:ExpressRequest} req
|
|
147
|
+
* @param {external:ExpressResponse} res
|
|
148
|
+
* @param {Function} next
|
|
149
|
+
*/
|
|
150
|
+
async authenticateHandler (req, res, next) {
|
|
151
|
+
const { email, persistSession } = req.body
|
|
152
|
+
const [user] = await this.users.find({ email })
|
|
153
|
+
if (!user) {
|
|
154
|
+
return res.sendError(this.app.errors.INVALID_LOGIN_DETAILS)
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
await this.authenticate(user, req, res)
|
|
158
|
+
|
|
159
|
+
if (req.session) {
|
|
160
|
+
if (persistSession !== true) req.session.cookie.maxAge = null
|
|
161
|
+
else this.log('debug', 'NEW_SESSION', user._id)
|
|
162
|
+
|
|
163
|
+
req.session.token = await AuthToken.generate(this.type, user)
|
|
164
|
+
}
|
|
165
|
+
res.status(204).json()
|
|
166
|
+
} catch (e) {
|
|
167
|
+
this.log('debug', 'FAILED_LOGIN', !user ? 'INVALID_USER' : user?._id?.toString(), this.app.lang.translate(undefined, e))
|
|
168
|
+
res.sendError(e)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Handles user enable/disable requests
|
|
174
|
+
* @param {external:ExpressRequest} req
|
|
175
|
+
* @param {external:ExpressResponse} res
|
|
176
|
+
* @param {Function} next
|
|
177
|
+
*/
|
|
178
|
+
async enableHandler (req, res, next) {
|
|
179
|
+
try {
|
|
180
|
+
const [user] = await this.users.find({ _id: req.body._id })
|
|
181
|
+
const isEnable = req.url === '/enable'
|
|
182
|
+
await this.setUserEnabled(user, isEnable)
|
|
183
|
+
this.log('debug', isEnable ? 'USER_ENABLE' : 'USER_DISABLE', user._id, req?.auth?.user?._id?.toString())
|
|
184
|
+
res.status(204).json()
|
|
185
|
+
} catch (e) {
|
|
186
|
+
return next(e)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Handles user registration requests
|
|
192
|
+
* @param {external:ExpressRequest} req
|
|
193
|
+
* @param {external:ExpressResponse} res
|
|
194
|
+
* @param {Function} next
|
|
195
|
+
*/
|
|
196
|
+
async registerHandler (req, res, next) {
|
|
197
|
+
try {
|
|
198
|
+
await this.registerHook.invoke(req)
|
|
199
|
+
const user = await this.register(req.body)
|
|
200
|
+
this.log('debug', 'USER_REG', user._id, req?.auth?.user?._id?.toString())
|
|
201
|
+
res.json(user)
|
|
202
|
+
} catch (e) {
|
|
203
|
+
return next(this.app.errors.USER_REG_FAILED.setData({ error: req.translate(e) }))
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export default AbstractAuthModule
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { AbstractModule } from 'adapt-authoring-core'
|
|
2
|
+
import Authentication from './Authentication.js'
|
|
3
|
+
import AuthToken from './AuthToken.js'
|
|
4
|
+
import AuthUtils from './AuthUtils.js'
|
|
5
|
+
import Permissions from './Permissions.js'
|
|
6
|
+
/**
|
|
7
|
+
* Adds authentication + authorisation to the server
|
|
8
|
+
* @memberof auth
|
|
9
|
+
* @extends {AbstractModule}
|
|
10
|
+
*/
|
|
11
|
+
class AuthModule extends AbstractModule {
|
|
12
|
+
/** @override */
|
|
13
|
+
async init () {
|
|
14
|
+
/**
|
|
15
|
+
* All routes to ignore auth
|
|
16
|
+
* @type {RouteStore}
|
|
17
|
+
* @example
|
|
18
|
+
* {
|
|
19
|
+
* post: { "/api/test": true }
|
|
20
|
+
* }
|
|
21
|
+
*/
|
|
22
|
+
this.unsecuredRoutes = AuthUtils.createEmptyStore()
|
|
23
|
+
/**
|
|
24
|
+
* Whether auth should be enabled
|
|
25
|
+
* @type {Boolean}
|
|
26
|
+
*/
|
|
27
|
+
this.isEnabled = this.getConfig('isEnabled')
|
|
28
|
+
|
|
29
|
+
if (!this.isEnabled) {
|
|
30
|
+
if (this.app.config.getConfig('env.NODE_ENV') !== 'production') {
|
|
31
|
+
this.log('info', 'auth disabled')
|
|
32
|
+
} else {
|
|
33
|
+
this.log('warn', 'cannot disable auth for production environments')
|
|
34
|
+
this.isEnabled = true
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const server = await this.app.waitForModule('server')
|
|
38
|
+
/**
|
|
39
|
+
* Reference to the Express router
|
|
40
|
+
* @type {Router}
|
|
41
|
+
*/
|
|
42
|
+
this.router = server.api.createChildRouter('auth')
|
|
43
|
+
|
|
44
|
+
server.root.addHandlerMiddleware(this.rootMiddleware.bind(this))
|
|
45
|
+
server.api.addHandlerMiddleware(this.apiMiddleware.bind(this))
|
|
46
|
+
/**
|
|
47
|
+
* The permission-checking unit
|
|
48
|
+
* @type {Permissions}
|
|
49
|
+
*/
|
|
50
|
+
this.permissions = await Permissions.init(this)
|
|
51
|
+
/**
|
|
52
|
+
* The authentication unit
|
|
53
|
+
* @type {Authentication}
|
|
54
|
+
*/
|
|
55
|
+
this.authentication = await Authentication.init(this)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Locks a route to only users with the passed permissions scopes
|
|
60
|
+
* @param {String} route The route
|
|
61
|
+
* @param {String} method The HTTP method
|
|
62
|
+
* @param {Array<String>} scopes Permissions scopes
|
|
63
|
+
*/
|
|
64
|
+
secureRoute (route, method, scopes) {
|
|
65
|
+
this.permissions.secureRoute(route, method, scopes)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Allows unconditional access to a specific route
|
|
70
|
+
* @type {Function}
|
|
71
|
+
* @param {String} route The route/endpoint
|
|
72
|
+
* @param {String} method HTTP method to allow
|
|
73
|
+
*/
|
|
74
|
+
unsecureRoute (route, method) {
|
|
75
|
+
this.unsecuredRoutes[method.toLowerCase()][route] = true
|
|
76
|
+
this.log('debug', 'UNSECURED_ROUTE', method.toUpperCase(), route)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Processes and parses incoming auth data
|
|
81
|
+
* @param {external:ExpressRequest} req
|
|
82
|
+
*/
|
|
83
|
+
async initAuthData (req) {
|
|
84
|
+
await AuthUtils.initAuthData(req)
|
|
85
|
+
if (this.isEnabled) await AuthToken.initRequestData(req)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Initialises auth data for root requests
|
|
90
|
+
* @param {external:ExpressRequest} req
|
|
91
|
+
* @param {external:ExpressResponse} res
|
|
92
|
+
* @param {Function} next
|
|
93
|
+
*/
|
|
94
|
+
rootMiddleware (req, res, next) {
|
|
95
|
+
this.initAuthData(req).then(next, () => next())
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Initialises auth data for root requests
|
|
100
|
+
* @param {external:ExpressRequest} req
|
|
101
|
+
* @param {external:ExpressResponse} res
|
|
102
|
+
* @param {Function} next
|
|
103
|
+
*/
|
|
104
|
+
async apiMiddleware (req, res, next) {
|
|
105
|
+
let initError
|
|
106
|
+
try {
|
|
107
|
+
await this.initAuthData(req)
|
|
108
|
+
} catch (e) {
|
|
109
|
+
initError = e
|
|
110
|
+
}
|
|
111
|
+
const method = req.method.toLowerCase()
|
|
112
|
+
const route = `${req.baseUrl}${req.route.path}`
|
|
113
|
+
const shortRoute = route.slice(0, route.lastIndexOf('/'))
|
|
114
|
+
const isUnsecured = this.unsecuredRoutes[method][route] || this.unsecuredRoutes[method][shortRoute]
|
|
115
|
+
|
|
116
|
+
if (initError && !isUnsecured) {
|
|
117
|
+
this.log('debug', 'BLOCK_REQUEST', req.originalUrl, initError.statusCode, req?.auth?.user?._id)
|
|
118
|
+
return res.sendError(initError)
|
|
119
|
+
}
|
|
120
|
+
if (!isUnsecured) {
|
|
121
|
+
await this.permissions.check(req)
|
|
122
|
+
}
|
|
123
|
+
next()
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export default AuthModule
|