UNPKG

7.66 kBJavaScriptView Raw
1'use strict'
2
3const debug = require('./../debug').authentication
4
5const AuthRequest = require('./auth-request')
6
7const url = require('url')
8const intoStream = require('into-stream')
9
10const $rdf = require('rdflib')
11const ACL = $rdf.Namespace('http://www.w3.org/ns/auth/acl#')
12
13/**
14 * Models a local Login request
15 */
16class SharingRequest extends AuthRequest {
17 /**
18 * @constructor
19 * @param options {Object}
20 *
21 * @param [options.response] {ServerResponse} middleware `res` object
22 * @param [options.session] {Session} req.session
23 * @param [options.userStore] {UserStore}
24 * @param [options.accountManager] {AccountManager}
25 * @param [options.returnToUrl] {string}
26 * @param [options.authQueryParams] {Object} Key/value hashmap of parsed query
27 * parameters that will be passed through to the /authorize endpoint.
28 * @param [options.authenticator] {Authenticator} Auth strategy by which to
29 * log in
30 */
31 constructor (options) {
32 super(options)
33
34 this.authenticator = options.authenticator
35 this.authMethod = options.authMethod
36 }
37
38 /**
39 * Factory method, returns an initialized instance of LoginRequest
40 * from an incoming http request.
41 *
42 * @param req {IncomingRequest}
43 * @param res {ServerResponse}
44 * @param authMethod {string}
45 *
46 * @return {LoginRequest}
47 */
48 static fromParams (req, res) {
49 let options = AuthRequest.requestOptions(req, res)
50
51 return new SharingRequest(options)
52 }
53
54 /**
55 * Handles a Login GET request on behalf of a middleware handler, displays
56 * the Login page.
57 * Usage:
58 *
59 * ```
60 * app.get('/login', LoginRequest.get)
61 * ```
62 *
63 * @param req {IncomingRequest}
64 * @param res {ServerResponse}
65 */
66 static async get (req, res) {
67 const request = SharingRequest.fromParams(req, res)
68
69 const appUrl = request.getAppUrl()
70 const appOrigin = appUrl.origin
71 const serverUrl = new url.URL(req.app.locals.ldp.serverUri)
72
73 // Check if is already registered or is data browser or the webId is not on this machine
74 if (request.isUserLoggedIn()) {
75 if (
76 !request.isSubdomain(serverUrl.host, new url.URL(request.session.subject._id).host) ||
77 (appUrl && request.isSubdomain(serverUrl.host, appUrl.host) && appUrl.protocol === serverUrl.protocol) ||
78 await request.isAppRegistered(req.app.locals.ldp, appOrigin, request.session.subject._id)
79 ) {
80 request.setUserShared(appOrigin)
81 request.redirectPostSharing()
82 } else {
83 request.renderForm(null, req, appOrigin)
84 }
85 } else {
86 request.redirectPostSharing()
87 }
88 }
89
90 /**
91 * Performs the login operation -- loads and validates the
92 * appropriate user, inits the session with credentials, and redirects the
93 * user to continue their auth flow.
94 *
95 * @param request {LoginRequest}
96 *
97 * @return {Promise}
98 */
99 static async share (req, res) {
100 let accessModes = []
101 let consented = false
102 if (req.body) {
103 accessModes = req.body.access_mode || []
104 if (!Array.isArray(accessModes)) {
105 accessModes = [ accessModes ]
106 }
107 consented = req.body.consent
108 }
109
110 let request = SharingRequest.fromParams(req, res)
111
112 if (request.isUserLoggedIn()) {
113 const appUrl = request.getAppUrl()
114 const appOrigin = `${appUrl.protocol}//${appUrl.host}`
115 debug('Sharing App')
116
117 if (consented) {
118 await request.registerApp(req.app.locals.ldp, appOrigin, accessModes, request.session.subject._id)
119 request.setUserShared(appOrigin)
120 }
121
122 // Redirect once that's all done
123 request.redirectPostSharing()
124 } else {
125 request.redirectPostSharing()
126 }
127 }
128
129 isSubdomain (domain, subdomain) {
130 const domainArr = domain.split('.')
131 const subdomainArr = subdomain.split('.')
132 for (let i = 1; i <= domainArr.length; i++) {
133 if (subdomainArr[subdomainArr.length - i] !== domainArr[domainArr.length - i]) {
134 return false
135 }
136 }
137 return true
138 }
139
140 setUserShared (appOrigin) {
141 if (!this.session.consentedOrigins) {
142 this.session.consentedOrigins = []
143 }
144 if (!this.session.consentedOrigins.includes(appOrigin)) {
145 this.session.consentedOrigins.push(appOrigin)
146 }
147 }
148
149 isUserLoggedIn () {
150 // Ensure the user arrived here by logging in
151 return !!(this.session.subject && this.session.subject._id)
152 }
153
154 getAppUrl () {
155 return new url.URL(this.authQueryParams.redirect_uri)
156 }
157
158 async getProfileGraph (ldp, webId) {
159 return await new Promise(async (resolve, reject) => {
160 const store = $rdf.graph()
161 const profileText = await ldp.readResource(webId)
162 $rdf.parse(profileText.toString(), store, this.getWebIdFile(webId), 'text/turtle', (error, kb) => {
163 if (error) {
164 reject(error)
165 } else {
166 resolve(kb)
167 }
168 })
169 })
170 }
171
172 async saveProfileGraph (ldp, store, webId) {
173 const text = $rdf.serialize(undefined, store, this.getWebIdFile(webId), 'text/turtle')
174 await ldp.put(webId, intoStream(text), 'text/turtle')
175 }
176
177 getWebIdFile (webId) {
178 const webIdurl = new url.URL(webId)
179 return `${webIdurl.origin}${webIdurl.pathname}`
180 }
181
182 async isAppRegistered (ldp, appOrigin, webId) {
183 const store = await this.getProfileGraph(ldp, webId)
184 return store.each($rdf.sym(webId), ACL('trustedApp')).find((app) => {
185 return store.each(app, ACL('origin')).find(rdfAppOrigin => rdfAppOrigin.value === appOrigin)
186 })
187 }
188
189 async registerApp (ldp, appOrigin, accessModes, webId) {
190 debug(`Registering app (${appOrigin}) with accessModes ${accessModes} for webId ${webId}`)
191 const store = await this.getProfileGraph(ldp, webId)
192 const origin = $rdf.sym(appOrigin)
193 // remove existing statements on same origin - if it exists
194 store.statementsMatching(null, ACL('origin'), origin).forEach(st => {
195 store.removeStatements([...store.statementsMatching(null, ACL('trustedApp'), st.subject)])
196 store.removeStatements([...store.statementsMatching(st.subject)])
197 })
198
199 // add new triples
200 const application = new $rdf.BlankNode()
201 store.add($rdf.sym(webId), ACL('trustedApp'), application, webId)
202 store.add(application, ACL('origin'), origin, webId)
203
204 accessModes.forEach(mode => {
205 store.add(application, ACL('mode'), ACL(mode))
206 })
207 await this.saveProfileGraph(ldp, store, webId)
208 }
209
210 /**
211 * Returns a URL to redirect the user to after login.
212 * Either uses the provided `redirect_uri` auth query param, or simply
213 * returns the user profile URI if none was provided.
214 *
215 * @param validUser {UserAccount}
216 *
217 * @return {string}
218 */
219 postSharingUrl () {
220 return this.authorizeUrl()
221 }
222
223 /**
224 * Redirects the Login request to continue on the OIDC auth workflow.
225 */
226 redirectPostSharing () {
227 let uri = this.postSharingUrl()
228 debug('Login successful, redirecting to ', uri)
229 this.response.redirect(uri)
230 }
231
232 /**
233 * Renders the login form
234 */
235 renderForm (error, req, appOrigin) {
236 let queryString = req && req.url && req.url.replace(/[^?]+\?/, '') || ''
237 let params = Object.assign({}, this.authQueryParams,
238 {
239 registerUrl: this.registerUrl(),
240 returnToUrl: this.returnToUrl,
241 enablePassword: this.localAuth.password,
242 enableTls: this.localAuth.tls,
243 tlsUrl: `/login/tls?${encodeURIComponent(queryString)}`,
244 app_origin: appOrigin
245 })
246
247 if (error) {
248 params.error = error.message
249 this.response.status(error.statusCode)
250 }
251
252 this.response.render('auth/sharing', params)
253 }
254}
255
256module.exports = {
257 SharingRequest
258}