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