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