Skip to content

Commit 7adbe1c

Browse files
authored
Merge pull request #1187 from solid/consent-screen
[WIP] Consent screen
2 parents 51cd35c + 953e326 commit 7adbe1c

File tree

14 files changed

+507
-80
lines changed

14 files changed

+507
-80
lines changed

common/disclaimer.html

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<html>
2+
<head>
3+
<title>Solid Consent Disclaimer</title>
4+
</head>
5+
<body>
6+
<h1 id="grantingaccesstoanapplicationaspartofauthentication">Granting access to an application as part of authentication</h1>
7+
8+
<p><strong>TL:DR: This is a temporary option that will be removed once we have better ways of granting access to applications. We recommend you grant read and write access by default, but it depends on the application you want to trust.</strong></p>
9+
10+
<p>Applications provide very useful ways of consuming and producing data. Solid provides functionality that allows you to grant access to applications that you trust. This trust might be misplaced sometimes though, which Solid tries to mitigate to lessen the harm that malicious applications can cause.</p>
11+
12+
<p>One of the strategies available in Solid is to check the origins of applications, and in solid-server in Node version 5 (NSS5) we set the configuration for this to be true by default. This strengthens the security of instances running on this codebase, but it also makes it more difficult for users to test applications without explicitly granting them access beforehand.</p>
13+
14+
<p>To facilitate a better user experience, we introduced the option of granting access to applications as part of the authentication process. We believe this is a <a href="https://github.com/solid/node-solid-server/issues/1142">better flow then forcing users to navigate to their profile and use the functionality provided in the trusted applications pane</a>, and offer this as a temporary solution.</p>
15+
16+
<h2 id="whichmodesshouldigranttheapplication">Which modes should I grant the application?</h2>
17+
18+
<p>That really depends on what the application needs to do. In general we suggest granting it Read and Write access. </p>
19+
20+
<p>This is what the various modes allows the application to do:</p>
21+
22+
<ul>
23+
<li><strong>Read:</strong> Allows the application to read resources - this includes navigating through your pod and potentially copy all of your data</li>
24+
25+
<li><strong>Write:</strong> Allows the application to change and delete resources</li>
26+
27+
<li><strong>Append:</strong> Allows the application to only append new content to resources, not remove existing content</li>
28+
29+
<li><strong>Control:</strong> Allows the application to set which access modes agents have (including themself) - by allowing this you essentially allow the application complete control of your pod</li>
30+
</ul>
31+
32+
<p>The last mode is a very powerful mode to grant an application. An application could use this to remove all of your control access, essentially locking you out of your pod. (This would also mean that the application couldn't get access to your pod though, as it is still relying on your authentication.)</p>
33+
34+
<h2 id="whyisittemporary">Why is it temporary?</h2>
35+
36+
<p>The way this solutions works "behind the scenes" is that you are granting the application access to all resources that you have access to and that is connected to your profile (in general this would be the pod that was created alongside your WebID). This is probably fine when you want to test an application that you or someone you trust are developing, but it's definitely not the granular access control we want to offer.</p>
37+
38+
<p>We do not have a solution ready yet, but <a href="https://github.com/solid/webid-oidc-spec">we are working on it</a>. When the solution is specified and implemented in NSS, we will remove the option in the login flow, as you would go through another process of granting applications access that would result in a more granular control.</p>
39+
40+
<h2 id="learnmore">Learn more</h2>
41+
42+
<p>The way that we handle access control in Solid is described in <a href="[http://solid.github.io/web-access-control-spec/](http://solid.github.io/web-access-control-spec/)">the Web Access Control specification (WAC)</a>. If you want to understand the reasoning for why we chose to turn origin checking on by default, you can read about it in the <a href="https://www.w3.org/community/solid/wiki/Meetings#20190307_1400CET">Meeting W3 Solid Community Group had March 7th 2019 (last point on the agenda)</a>.</p>
43+
</body>
44+
</html>

default-views/auth/consent.hbs

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,39 @@
1010
</head>
1111
<body>
1212
<div class="container title">
13-
<div class="page-title">
14-
<h1>Authorize app to use your Web ID?</h1>
15-
</div>
13+
<h1>Authorize this app to use your data?</h1>
14+
<div class="panel panel-default">
15+
<div class="panel-body">
16+
<div class="page-title">
17+
<p>You will be authorizing <strong>{{app_origin}}</strong> to have access to perform the actions indicated below.</p>
18+
<p>NOTE: This screen is TEMPORARY. Eventually more fine-tuned controls will be available.</p>
19+
<p>For more information see the <a href="/common/disclaimer.html" target="_blank">full explanation</a>.</p>
20+
</div>
21+
<form method="post" action="/consent">
22+
23+
<input id="read" type="checkbox" name="access_mode" value="Read" checked>
24+
<label for="read">Read your data</label>
25+
<br>
26+
27+
<input id="write" type="checkbox" name="access_mode" value="Write" checked>
28+
<label for="write">Write new data</label>
29+
<br>
1630

17-
<form method="post" action="/authorize">
18-
<input type="hidden" name="response_type" value="{{response_type}}"/>
19-
<input type="hidden" name="display" value="{{display}}"/>
20-
<input type="hidden" name="scope" value="{{scope}}"/>
21-
<input type="hidden" name="client_id" value="{{client_id}}"/>
22-
<input type="hidden" name="redirect_uri" value="{{redirect_uri}}"/>
23-
<input type="hidden" name="state" value="{{state}}"/>
24-
<input type="hidden" name="nonce" value="{{nonce}}"/>
31+
<input id="append" type="checkbox" name="access_mode" value="Append" checked>
32+
<label for="append">Add to existing data</label>
33+
<br>
2534

26-
<button type="submit" class="btn btn-primary" name="consent" value="true">Authorize</button>
27-
<button type="submit" class="btn btn-default" name="cancel" value="true">Cancel</button>
28-
</form>
35+
<input id="control" type="checkbox" name="access_mode" value="Control">
36+
<label for="control">Control who can access your data</label>
37+
<br>
38+
<br>
39+
40+
<button type="submit" class="btn btn-primary" name="consent" value="true">Authorize</button>
41+
<button type="submit" class="btn btn-default" name="cancel" value="true">Cancel</button>
42+
{{> auth/auth-hidden-fields}}
43+
</form>
44+
</div>
45+
</div>
2946
</div>
3047
</body>
3148
</html>

lib/api/authn/webid-oidc.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const { routeResolvedFile } = require('../../utils')
88
const bodyParser = require('body-parser').urlencoded({ extended: false })
99
const OidcManager = require('../../models/oidc-manager')
1010
const { LoginRequest } = require('../../requests/login-request')
11+
const { ConsentRequest } = require('../../requests/consent-request')
1112

1213
const restrictToTopDomain = require('../../handlers/restrict-to-top-domain')
1314

@@ -83,6 +84,9 @@ function middleware (oidc) {
8384

8485
router.post('/login/tls', bodyParser, LoginRequest.loginTls)
8586

87+
router.get('/consent', ConsentRequest.get)
88+
router.post('/consent', bodyParser, ConsentRequest.giveConsent)
89+
8690
router.get('/account/password/reset', restrictToTopDomain, PasswordResetEmailRequest.get)
8791
router.post('/account/password/reset', restrictToTopDomain, bodyParser, PasswordResetEmailRequest.post)
8892

lib/requests/auth-request.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
const url = require('url')
44
const debug = require('./../debug').authentication
55

6+
const IDToken = require('@solid/oidc-op/src/IDToken')
7+
68
/**
79
* Hidden form fields from the login page that must be passed through to the
810
* Authentication request.
@@ -134,6 +136,11 @@ class AuthRequest {
134136
extracted[p] = value
135137
}
136138

139+
// Special case because solid-auth-client does not include redirect in params
140+
if (!extracted['redirect_uri'] && params.request) {
141+
extracted['redirect_uri'] = IDToken.decode(params.request).payload.redirect_uri
142+
}
143+
137144
return extracted
138145
}
139146

@@ -210,6 +217,15 @@ class AuthRequest {
210217

211218
return url.format(signupUrl)
212219
}
220+
221+
consentUrl () {
222+
let host = this.accountManager.host
223+
let consentUrl = url.parse(url.resolve(host.serverUri, '/consent'))
224+
225+
consentUrl.query = this.authQueryParams
226+
227+
return url.format(consentUrl)
228+
}
213229
}
214230

215231
AuthRequest.AUTH_QUERY_PARAMS = AUTH_QUERY_PARAMS

lib/requests/consent-request.js

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
'use strict'
2+
3+
const debug = require('./../debug').authentication
4+
5+
const AuthRequest = require('./auth-request')
6+
7+
const url = require('url')
8+
const intoStream = require('into-stream')
9+
10+
const $rdf = require('rdflib')
11+
const ACL = $rdf.Namespace('http://www.w3.org/ns/auth/acl#')
12+
13+
/**
14+
* Models a local Login request
15+
*/
16+
class ConsentRequest 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 ConsentRequest(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 = ConsentRequest.fromParams(req, res)
68+
69+
const appOrigin = request.getAppOrigin()
70+
// Check if is already registered or is data browser
71+
if (request.isUserLoggedIn()) {
72+
if (
73+
appOrigin === req.app.locals.ldp.serverUri ||
74+
await request.isAppRegistered(req.app.locals.ldp, appOrigin, request.session.subject._id)
75+
) {
76+
request.setUserConsent(appOrigin)
77+
request.redirectPostConsent()
78+
} else {
79+
request.renderForm(null, req)
80+
}
81+
}
82+
}
83+
84+
/**
85+
* Performs the login operation -- loads and validates the
86+
* appropriate user, inits the session with credentials, and redirects the
87+
* user to continue their auth flow.
88+
*
89+
* @param request {LoginRequest}
90+
*
91+
* @return {Promise}
92+
*/
93+
static async giveConsent (req, res) {
94+
let accessModes = []
95+
let consented = false
96+
if (req.body) {
97+
accessModes = req.body.access_mode
98+
consented = req.body.consent
99+
}
100+
101+
let request = ConsentRequest.fromParams(req, res)
102+
103+
if (request.isUserLoggedIn()) {
104+
const appOrigin = request.getAppOrigin()
105+
debug('Providing consent for app sharing')
106+
107+
if (consented) {
108+
await request.registerApp(req.app.locals.ldp, appOrigin, accessModes, request.session.subject._id)
109+
request.setUserConsent(appOrigin)
110+
}
111+
112+
// Redirect once that's all done
113+
request.redirectPostConsent()
114+
}
115+
}
116+
117+
setUserConsent (appOrigin) {
118+
if (!this.session.consentedOrigins) {
119+
this.session.consentedOrigins = []
120+
}
121+
if (!this.session.consentedOrigins.includes(appOrigin)) {
122+
this.session.consentedOrigins.push(appOrigin)
123+
}
124+
}
125+
126+
isUserLoggedIn () {
127+
// Ensure the user arrived here by logging in
128+
if (!this.session.subject || !this.session.subject._id) {
129+
this.response.status(401)
130+
this.response.send('User not logged in 2')
131+
return false
132+
}
133+
return true
134+
}
135+
136+
getAppOrigin () {
137+
const parsed = url.parse(this.authQueryParams.redirect_uri)
138+
return `${parsed.protocol}//${parsed.host}`
139+
}
140+
141+
async getProfileGraph (ldp, webId) {
142+
return await new Promise(async (resolve, reject) => {
143+
const store = $rdf.graph()
144+
const profileText = await ldp.readResource(webId)
145+
$rdf.parse(profileText.toString(), store, 'https://localhost:8443/profile/card', 'text/turtle', (error, kb) => {
146+
if (error) {
147+
reject(error)
148+
} else {
149+
resolve(kb)
150+
}
151+
})
152+
})
153+
}
154+
155+
async saveProfileGraph (ldp, store, webId) {
156+
const text = $rdf.serialize(undefined, store, webId, 'text/turtle')
157+
await ldp.put(webId, intoStream(text), 'text/turtle')
158+
}
159+
160+
async isAppRegistered (ldp, appOrigin, webId) {
161+
const store = await this.getProfileGraph(ldp, webId)
162+
return store.each($rdf.sym(webId), ACL('trustedApp')).find((app) => {
163+
return store.each(app, ACL('origin')).find(rdfAppOrigin => rdfAppOrigin.value === appOrigin)
164+
})
165+
}
166+
167+
async registerApp (ldp, appOrigin, accessModes, webId) {
168+
const store = await this.getProfileGraph(ldp, webId)
169+
const origin = $rdf.sym(appOrigin)
170+
// remove existing statements on same origin - if it exists
171+
store.statementsMatching(null, ACL('origin'), origin).forEach(st => {
172+
store.removeStatements([...store.statementsMatching(null, ACL('trustedApp'), st.subject)])
173+
store.removeStatements([...store.statementsMatching(st.subject)])
174+
})
175+
176+
// add new triples
177+
const application = new $rdf.BlankNode()
178+
store.add($rdf.sym(webId), ACL('trustedApp'), application, webId)
179+
store.add(application, ACL('origin'), origin, webId)
180+
accessModes.forEach(mode => {
181+
store.add(application, ACL('mode'), ACL(mode))
182+
})
183+
await this.saveProfileGraph(ldp, store, webId)
184+
}
185+
186+
/**
187+
* Returns a URL to redirect the user to after login.
188+
* Either uses the provided `redirect_uri` auth query param, or simply
189+
* returns the user profile URI if none was provided.
190+
*
191+
* @param validUser {UserAccount}
192+
*
193+
* @return {string}
194+
*/
195+
postConsentUrl () {
196+
return this.authorizeUrl()
197+
}
198+
199+
/**
200+
* Redirects the Login request to continue on the OIDC auth workflow.
201+
*/
202+
redirectPostConsent () {
203+
let uri = this.postConsentUrl()
204+
debug('Login successful, redirecting to ', uri)
205+
this.response.redirect(uri)
206+
}
207+
208+
/**
209+
* Renders the login form
210+
*/
211+
renderForm (error, req) {
212+
let queryString = req && req.url && req.url.replace(/[^?]+\?/, '') || ''
213+
let params = Object.assign({}, this.authQueryParams,
214+
{
215+
registerUrl: this.registerUrl(),
216+
returnToUrl: this.returnToUrl,
217+
enablePassword: this.localAuth.password,
218+
enableTls: this.localAuth.tls,
219+
tlsUrl: `/login/tls?${encodeURIComponent(queryString)}`
220+
})
221+
222+
if (error) {
223+
params.error = error.message
224+
this.response.status(error.statusCode)
225+
}
226+
227+
this.response.render('auth/consent', params)
228+
}
229+
}
230+
231+
module.exports = {
232+
ConsentRequest
233+
}

0 commit comments

Comments
 (0)