OLD | NEW |
| (Empty) |
1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | |
2 // for details. All rights reserved. Use of this source code is governed by a | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 library pub.oauth2; | |
6 | |
7 import 'dart:async'; | |
8 import 'dart:io'; | |
9 | |
10 import 'package:oauth2/oauth2.dart'; | |
11 import 'package:path/path.dart' as path; | |
12 import 'package:shelf/shelf.dart' as shelf; | |
13 import 'package:shelf/shelf_io.dart' as shelf_io; | |
14 | |
15 import 'http.dart'; | |
16 import 'io.dart'; | |
17 import 'log.dart' as log; | |
18 import 'system_cache.dart'; | |
19 import 'utils.dart'; | |
20 | |
21 export 'package:oauth2/oauth2.dart'; | |
22 | |
23 /// The pub client's OAuth2 identifier. | |
24 final _identifier = '818368855108-8grd2eg9tj9f38os6f1urbcvsq399u8n.apps.' | |
25 'googleusercontent.com'; | |
26 | |
27 /// The pub client's OAuth2 secret. | |
28 /// | |
29 /// This isn't actually meant to be kept a secret. | |
30 final _secret = 'SWeqj8seoJW0w7_CpEPFLX0K'; | |
31 | |
32 /// The URL to which the user will be directed to authorize the pub client to | |
33 /// get an OAuth2 access token. | |
34 /// | |
35 /// `access_type=offline` and `approval_prompt=force` ensures that we always get | |
36 /// a refresh token from the server. See the [Google OAuth2 documentation][]. | |
37 /// | |
38 /// [Google OAuth2 documentation]: https://developers.google.com/accounts/docs/O
Auth2WebServer#offline | |
39 final authorizationEndpoint = Uri.parse( | |
40 'https://accounts.google.com/o/oauth2/auth?access_type=offline' | |
41 '&approval_prompt=force'); | |
42 | |
43 /// The URL from which the pub client will request an access token once it's | |
44 /// been authorized by the user. | |
45 /// | |
46 /// This can be controlled externally by setting the `_PUB_TEST_TOKEN_ENDPOINT` | |
47 /// environment variable. | |
48 Uri get tokenEndpoint { | |
49 var tokenEndpoint = Platform.environment['_PUB_TEST_TOKEN_ENDPOINT']; | |
50 if (tokenEndpoint != null) { | |
51 return Uri.parse(tokenEndpoint); | |
52 } else { | |
53 return _tokenEndpoint; | |
54 } | |
55 } | |
56 | |
57 final _tokenEndpoint = Uri.parse('https://accounts.google.com/o/oauth2/token'); | |
58 | |
59 /// The OAuth2 scopes that the pub client needs. | |
60 /// | |
61 /// Currently the client only needs the user's email so that the server can | |
62 /// verify their identity. | |
63 final _scopes = ['https://www.googleapis.com/auth/userinfo.email']; | |
64 | |
65 /// An in-memory cache of the user's OAuth2 credentials. | |
66 /// | |
67 /// This should always be the same as the credentials file stored in the system | |
68 /// cache. | |
69 Credentials _credentials; | |
70 | |
71 /// Delete the cached credentials, if they exist. | |
72 void clearCredentials(SystemCache cache) { | |
73 _credentials = null; | |
74 var credentialsFile = _credentialsFile(cache); | |
75 if (entryExists(credentialsFile)) deleteEntry(credentialsFile); | |
76 } | |
77 | |
78 /// Asynchronously passes an OAuth2 [Client] to [fn], and closes the client when | |
79 /// the [Future] returned by [fn] completes. | |
80 /// | |
81 /// This takes care of loading and saving the client's credentials, as well as | |
82 /// prompting the user for their authorization. It will also re-authorize and | |
83 /// re-run [fn] if a recoverable authorization error is detected. | |
84 Future withClient(SystemCache cache, Future fn(Client client)) { | |
85 return _getClient(cache).then((client) { | |
86 return fn(client).whenComplete(() { | |
87 client.close(); | |
88 // Be sure to save the credentials even when an error happens. | |
89 _saveCredentials(cache, client.credentials); | |
90 }); | |
91 }).catchError((error) { | |
92 if (error is ExpirationException) { | |
93 log.error("Pub's authorization to upload packages has expired and " | |
94 "can't be automatically refreshed."); | |
95 return withClient(cache, fn); | |
96 } else if (error is AuthorizationException) { | |
97 var message = "OAuth2 authorization failed"; | |
98 if (error.description != null) { | |
99 message = "$message (${error.description})"; | |
100 } | |
101 log.error("$message."); | |
102 clearCredentials(cache); | |
103 return withClient(cache, fn); | |
104 } else { | |
105 throw error; | |
106 } | |
107 }); | |
108 } | |
109 | |
110 /// Gets a new OAuth2 client. | |
111 /// | |
112 /// If saved credentials are available, those are used; otherwise, the user is | |
113 /// prompted to authorize the pub client. | |
114 Future<Client> _getClient(SystemCache cache) { | |
115 return new Future.sync(() { | |
116 var credentials = _loadCredentials(cache); | |
117 if (credentials == null) return _authorize(); | |
118 | |
119 var client = new Client(_identifier, _secret, credentials, | |
120 httpClient: httpClient); | |
121 _saveCredentials(cache, client.credentials); | |
122 return client; | |
123 }); | |
124 } | |
125 | |
126 /// Loads the user's OAuth2 credentials from the in-memory cache or the | |
127 /// filesystem if possible. | |
128 /// | |
129 /// If the credentials can't be loaded for any reason, the returned [Future] | |
130 /// completes to `null`. | |
131 Credentials _loadCredentials(SystemCache cache) { | |
132 log.fine('Loading OAuth2 credentials.'); | |
133 | |
134 try { | |
135 if (_credentials != null) return _credentials; | |
136 | |
137 var path = _credentialsFile(cache); | |
138 if (!fileExists(path)) return null; | |
139 | |
140 var credentials = new Credentials.fromJson(readTextFile(path)); | |
141 if (credentials.isExpired && !credentials.canRefresh) { | |
142 log.error("Pub's authorization to upload packages has expired and " | |
143 "can't be automatically refreshed."); | |
144 return null; // null means re-authorize. | |
145 } | |
146 | |
147 return credentials; | |
148 } catch (e) { | |
149 log.error('Warning: could not load the saved OAuth2 credentials: $e\n' | |
150 'Obtaining new credentials...'); | |
151 return null; // null means re-authorize. | |
152 } | |
153 } | |
154 | |
155 /// Save the user's OAuth2 credentials to the in-memory cache and the | |
156 /// filesystem. | |
157 void _saveCredentials(SystemCache cache, Credentials credentials) { | |
158 log.fine('Saving OAuth2 credentials.'); | |
159 _credentials = credentials; | |
160 var credentialsPath = _credentialsFile(cache); | |
161 ensureDir(path.dirname(credentialsPath)); | |
162 writeTextFile(credentialsPath, credentials.toJson(), dontLogContents: true); | |
163 } | |
164 | |
165 /// The path to the file in which the user's OAuth2 credentials are stored. | |
166 String _credentialsFile(SystemCache cache) => | |
167 path.join(cache.rootDir, 'credentials.json'); | |
168 | |
169 /// Gets the user to authorize pub as a client of pub.dartlang.org via oauth2. | |
170 /// | |
171 /// Returns a Future that completes to a fully-authorized [Client]. | |
172 Future<Client> _authorize() { | |
173 var grant = new AuthorizationCodeGrant( | |
174 _identifier, | |
175 _secret, | |
176 authorizationEndpoint, | |
177 tokenEndpoint, | |
178 httpClient: httpClient); | |
179 | |
180 // Spin up a one-shot HTTP server to receive the authorization code from the | |
181 // Google OAuth2 server via redirect. This server will close itself as soon as | |
182 // the code is received. | |
183 var completer = new Completer(); | |
184 bindServer('localhost', 0).then((server) { | |
185 shelf_io.serveRequests(server, (request) { | |
186 if (request.url.path != "/") { | |
187 return new shelf.Response.notFound('Invalid URI.'); | |
188 } | |
189 | |
190 log.message('Authorization received, processing...'); | |
191 var queryString = request.url.query; | |
192 if (queryString == null) queryString = ''; | |
193 | |
194 // Closing the server here is safe, since it will wait until the response | |
195 // is sent to actually shut down. | |
196 server.close(); | |
197 chainToCompleter(grant.handleAuthorizationResponse(queryToMap(queryString)
), | |
198 completer); | |
199 | |
200 return new shelf.Response.found('http://pub.dartlang.org/authorized'); | |
201 }); | |
202 | |
203 var authUrl = grant.getAuthorizationUrl( | |
204 Uri.parse('http://localhost:${server.port}'), scopes: _scopes); | |
205 | |
206 log.message( | |
207 'Pub needs your authorization to upload packages on your behalf.\n' | |
208 'In a web browser, go to $authUrl\n' | |
209 'Then click "Allow access".\n\n' | |
210 'Waiting for your authorization...'); | |
211 }); | |
212 | |
213 return completer.future.then((client) { | |
214 log.message('Successfully authorized.\n'); | |
215 return client; | |
216 }); | |
217 } | |
OLD | NEW |