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 ADDED
@@ -0,0 +1 @@
1
+ node_modules
package/.eslintrc ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "env": {
3
+ "browser": false,
4
+ "node": true,
5
+ "commonjs": false,
6
+ "es2020": true
7
+ },
8
+ "extends": [
9
+ "standard"
10
+ ],
11
+ "parserOptions": {
12
+ "ecmaVersion": 2020
13
+ }
14
+ }
@@ -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,5 @@
1
+ {
2
+ "documentation": {
3
+ "enable": true
4
+ }
5
+ }
@@ -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