Devon Carew | 4172ae5 | 2022-10-25 00:54:31 | [diff] [blame] | 1 | [](https://github.com/dart-lang/oauth2/actions/workflows/test-package.yml) |
| 2 | [](https://pub.dev/packages/oauth2) |
| 3 | [](https://pub.dev/packages/oauth2/publisher) |
| 4 | |
Nate Bosch | 1f4a79d | 2020-07-20 21:16:16 | [diff] [blame] | 5 | A client library for authenticating with a remote service via OAuth2 on behalf |
| 6 | of a user, and making authorized HTTP requests with the user's OAuth2 |
[email protected] | 9c0b554 | 2014-05-05 20:25:13 | [diff] [blame] | 7 | credentials. |
[email protected] | c16d766 | 2014-03-26 22:18:10 | [diff] [blame] | 8 | |
Devon Carew | 4172ae5 | 2022-10-25 00:54:31 | [diff] [blame] | 9 | ## About OAuth2 |
| 10 | |
Nate Bosch | 1f4a79d | 2020-07-20 21:16:16 | [diff] [blame] | 11 | OAuth2 allows a client (the program using this library) to access and manipulate |
| 12 | a resource that's owned by a resource owner (the end user) and lives on a remote |
| 13 | server. The client directs the resource owner to an authorization server |
| 14 | (usually but not always the same as the server that hosts the resource), where |
| 15 | the resource owner tells the authorization server to give the client an access |
| 16 | token. This token serves as proof that the client has permission to access |
| 17 | resources on behalf of the resource owner. |
[email protected] | c16d766 | 2014-03-26 22:18:10 | [diff] [blame] | 18 | |
| 19 | OAuth2 provides several different methods for the client to obtain |
| 20 | authorization. At the time of writing, this library only supports the |
Nate Bosch | 1f4a79d | 2020-07-20 21:16:16 | [diff] [blame] | 21 | [Authorization Code Grant][authorizationCodeGrantSection], |
| 22 | [Client Credentials Grant][clientCredentialsGrantSection] and |
| 23 | [Resource Owner Password Grant][resourceOwnerPasswordGrantSection] flows, but |
| 24 | more may be added in the future. |
[email protected] | c16d766 | 2014-03-26 22:18:10 | [diff] [blame] | 25 | |
Natalie Weizenbaum | 1e90128 | 2015-09-14 19:55:56 | [diff] [blame] | 26 | ## Authorization Code Grant |
Nate Bosch | 1f4a79d | 2020-07-20 21:16:16 | [diff] [blame] | 27 | |
| 28 | **Resources:** [Class summary][authorizationCodeGrantMethod], |
| 29 | [OAuth documentation][authorizationCodeGrantDocs] |
Natalie Weizenbaum | 1e90128 | 2015-09-14 19:55:56 | [diff] [blame] | 30 | |
[email protected] | c16d766 | 2014-03-26 22:18:10 | [diff] [blame] | 31 | ```dart |
ovo-6 | 4773d80 | 2018-01-01 22:06:30 | [diff] [blame] | 32 | import 'dart:io'; |
Nate Bosch | 1f4a79d | 2020-07-20 21:16:16 | [diff] [blame] | 33 | |
[email protected] | c16d766 | 2014-03-26 22:18:10 | [diff] [blame] | 34 | import 'package:oauth2/oauth2.dart' as oauth2; |
| 35 | |
| 36 | // These URLs are endpoints that are provided by the authorization |
| 37 | // server. They're usually included in the server's documentation of its |
| 38 | // OAuth2 API. |
| 39 | final authorizationEndpoint = |
Nate Bosch | 1f4a79d | 2020-07-20 21:16:16 | [diff] [blame] | 40 | Uri.parse('http://example.com/oauth2/authorization'); |
| 41 | final tokenEndpoint = Uri.parse('http://example.com/oauth2/token'); |
[email protected] | c16d766 | 2014-03-26 22:18:10 | [diff] [blame] | 42 | |
| 43 | // The authorization server will issue each client a separate client |
| 44 | // identifier and secret, which allows the server to tell which client |
| 45 | // is accessing it. Some servers may also have an anonymous |
| 46 | // identifier/secret pair that any client may use. |
| 47 | // |
| 48 | // Note that clients whose source code or binary executable is readily |
| 49 | // available may not be able to make sure the client secret is kept a |
| 50 | // secret. This is fine; OAuth2 servers generally won't rely on knowing |
| 51 | // with certainty that a client is who it claims to be. |
Nate Bosch | 1f4a79d | 2020-07-20 21:16:16 | [diff] [blame] | 52 | final identifier = 'my client identifier'; |
| 53 | final secret = 'my client secret'; |
[email protected] | c16d766 | 2014-03-26 22:18:10 | [diff] [blame] | 54 | |
| 55 | // This is a URL on your application's server. The authorization server |
| 56 | // will redirect the resource owner here once they've authorized the |
| 57 | // client. The redirection will include the authorization code in the |
| 58 | // query parameters. |
Nate Bosch | 1f4a79d | 2020-07-20 21:16:16 | [diff] [blame] | 59 | final redirectUrl = Uri.parse('http://my-site.com/oauth2-redirect'); |
[email protected] | c16d766 | 2014-03-26 22:18:10 | [diff] [blame] | 60 | |
Natalie Weizenbaum | 0589889 | 2015-08-24 23:58:43 | [diff] [blame] | 61 | /// A file in which the users credentials are stored persistently. If the server |
| 62 | /// issues a refresh token allowing the client to refresh outdated credentials, |
| 63 | /// these may be valid indefinitely, meaning the user never has to |
| 64 | /// re-authenticate. |
Nate Bosch | 1f4a79d | 2020-07-20 21:16:16 | [diff] [blame] | 65 | final credentialsFile = File('~/.myapp/credentials.json'); |
Natalie Weizenbaum | 0589889 | 2015-08-24 23:58:43 | [diff] [blame] | 66 | |
| 67 | /// Either load an OAuth2 client from saved credentials or authenticate a new |
| 68 | /// one. |
Nate Bosch | 1f4a79d | 2020-07-20 21:16:16 | [diff] [blame] | 69 | Future<oauth2.Client> createClient() async { |
Natalie Weizenbaum | 0589889 | 2015-08-24 23:58:43 | [diff] [blame] | 70 | var exists = await credentialsFile.exists(); |
| 71 | |
| 72 | // If the OAuth2 credentials have already been saved from a previous run, we |
| 73 | // just want to reload them. |
[email protected] | c16d766 | 2014-03-26 22:18:10 | [diff] [blame] | 74 | if (exists) { |
Nate Bosch | 1f4a79d | 2020-07-20 21:16:16 | [diff] [blame] | 75 | var credentials = |
| 76 | oauth2.Credentials.fromJson(await credentialsFile.readAsString()); |
| 77 | return oauth2.Client(credentials, identifier: identifier, secret: secret); |
[email protected] | c16d766 | 2014-03-26 22:18:10 | [diff] [blame] | 78 | } |
| 79 | |
Natalie Weizenbaum | 0589889 | 2015-08-24 23:58:43 | [diff] [blame] | 80 | // If we don't have OAuth2 credentials yet, we need to get the resource owner |
| 81 | // to authorize us. We're assuming here that we're a command-line application. |
Nate Bosch | 1f4a79d | 2020-07-20 21:16:16 | [diff] [blame] | 82 | var grant = oauth2.AuthorizationCodeGrant( |
Natalie Weizenbaum | 45f3628 | 2015-08-26 20:47:18 | [diff] [blame] | 83 | identifier, authorizationEndpoint, tokenEndpoint, |
| 84 | secret: secret); |
[email protected] | c16d766 | 2014-03-26 22:18:10 | [diff] [blame] | 85 | |
Levi Hassel | 6649c87 | 2020-04-27 09:12:26 | [diff] [blame] | 86 | // A URL on the authorization server (authorizationEndpoint with some additional |
| 87 | // query parameters). Scopes and state can optionally be passed into this method. |
| 88 | var authorizationUrl = grant.getAuthorizationUrl(redirectUrl); |
| 89 | |
| 90 | // Redirect the resource owner to the authorization URL. Once the resource |
| 91 | // owner has authorized, they'll be redirected to `redirectUrl` with an |
Nate Bosch | 1f4a79d | 2020-07-20 21:16:16 | [diff] [blame] | 92 | // authorization code. The `redirect` should cause the browser to redirect to |
| 93 | // another URL which should also have a listener. |
[email protected] | c16d766 | 2014-03-26 22:18:10 | [diff] [blame] | 94 | // |
Nate Bosch | 1f4a79d | 2020-07-20 21:16:16 | [diff] [blame] | 95 | // `redirect` and `listen` are not shown implemented here. See below for the |
| 96 | // details. |
Levi Hassel | 6649c87 | 2020-04-27 09:12:26 | [diff] [blame] | 97 | await redirect(authorizationUrl); |
Levi Hassel | 6649c87 | 2020-04-27 09:12:26 | [diff] [blame] | 98 | var responseUrl = await listen(redirectUrl); |
Natalie Weizenbaum | 0589889 | 2015-08-24 23:58:43 | [diff] [blame] | 99 | |
| 100 | // Once the user is redirected to `redirectUrl`, pass the query parameters to |
| 101 | // the AuthorizationCodeGrant. It will validate them and extract the |
| 102 | // authorization code to create a new Client. |
Levi Hassel | 6649c87 | 2020-04-27 09:12:26 | [diff] [blame] | 103 | return await grant.handleAuthorizationResponse(responseUrl.queryParameters); |
Natalie Weizenbaum | 0589889 | 2015-08-24 23:58:43 | [diff] [blame] | 104 | } |
| 105 | |
Nate Bosch | 1f4a79d | 2020-07-20 21:16:16 | [diff] [blame] | 106 | void main() async { |
| 107 | var client = await createClient(); |
Natalie Weizenbaum | 0589889 | 2015-08-24 23:58:43 | [diff] [blame] | 108 | |
| 109 | // Once you have a Client, you can use it just like any other HTTP client. |
Nate Bosch | 1f4a79d | 2020-07-20 21:16:16 | [diff] [blame] | 110 | print(await client.read('http://example.com/protected-resources.txt')); |
Natalie Weizenbaum | 0589889 | 2015-08-24 23:58:43 | [diff] [blame] | 111 | |
| 112 | // Once we're done with the client, save the credentials file. This ensures |
| 113 | // that if the credentials were automatically refreshed while using the |
| 114 | // client, the new credentials are available for the next run of the |
| 115 | // program. |
| 116 | await credentialsFile.writeAsString(client.credentials.toJson()); |
Natalie Weizenbaum | 0589889 | 2015-08-24 23:58:43 | [diff] [blame] | 117 | } |
[email protected] | c16d766 | 2014-03-26 22:18:10 | [diff] [blame] | 118 | ``` |
Erik Grimes | c658507 | 2015-09-14 19:55:18 | [diff] [blame] | 119 | |
Levi Hassel | 6649c87 | 2020-04-27 09:12:26 | [diff] [blame] | 120 | <details> |
Nate Bosch | 1f4a79d | 2020-07-20 21:16:16 | [diff] [blame] | 121 | <summary>Click here to learn how to implement `redirect` and `listen`.</summary> |
Levi Hassel | 6649c87 | 2020-04-27 09:12:26 | [diff] [blame] | 122 | |
Nate Bosch | 1f4a79d | 2020-07-20 21:16:16 | [diff] [blame] | 123 | -------------------------------------------------------------------------------- |
| 124 | |
| 125 | There is not a universal example for implementing `redirect` and `listen`, |
| 126 | because different options exist for each platform. |
| 127 | |
| 128 | For Flutter apps, there's two popular approaches: |
| 129 | |
| 130 | 1. Launch a browser using [url_launcher][] and listen for a redirect using |
| 131 | [uni_links][]. |
| 132 | |
| 133 | ```dart |
| 134 | if (await canLaunch(authorizationUrl.toString())) { |
| 135 | await launch(authorizationUrl.toString()); } |
| 136 | |
| 137 | // ------- 8< ------- |
| 138 | |
| 139 | final linksStream = getLinksStream().listen((Uri uri) async { |
| 140 | if (uri.toString().startsWith(redirectUrl)) { |
| 141 | responseUrl = uri; |
| 142 | } |
| 143 | }); |
| 144 | ``` |
| 145 | |
| 146 | 1. Launch a WebView inside the app and listen for a redirect using |
| 147 | [webview_flutter][]. |
| 148 | |
| 149 | ```dart |
| 150 | WebView( |
| 151 | javascriptMode: JavascriptMode.unrestricted, |
| 152 | initialUrl: authorizationUrl.toString(), |
| 153 | navigationDelegate: (navReq) { |
| 154 | if (navReq.url.startsWith(redirectUrl)) { |
| 155 | responseUrl = Uri.parse(navReq.url); |
| 156 | return NavigationDecision.prevent; |
Levi Hassel | 6649c87 | 2020-04-27 09:12:26 | [diff] [blame] | 157 | } |
Nate Bosch | 1f4a79d | 2020-07-20 21:16:16 | [diff] [blame] | 158 | return NavigationDecision.navigate; |
| 159 | }, |
| 160 | // ------- 8< ------- |
| 161 | ); |
| 162 | ``` |
Levi Hassel | 6649c87 | 2020-04-27 09:12:26 | [diff] [blame] | 163 | |
Nate Bosch | 1f4a79d | 2020-07-20 21:16:16 | [diff] [blame] | 164 | For Dart apps, the best approach depends on the available options for accessing |
| 165 | a browser. In general, you'll need to launch the authorization URL through the |
| 166 | client's browser and listen for the redirect URL. |
Levi Hassel | 6649c87 | 2020-04-27 09:12:26 | [diff] [blame] | 167 | </details> |
| 168 | |
Tobe Osakwe | 46d0f74 | 2019-11-19 22:43:13 | [diff] [blame] | 169 | ## Client Credentials Grant |
Nate Bosch | 1f4a79d | 2020-07-20 21:16:16 | [diff] [blame] | 170 | |
| 171 | **Resources:** [Method summary][clientCredentialsGrantMethod], |
| 172 | [OAuth documentation][clientCredentialsGrantDocs] |
Levi Hassel | 6649c87 | 2020-04-27 09:12:26 | [diff] [blame] | 173 | |
Tobe Osakwe | 46d0f74 | 2019-11-19 22:43:13 | [diff] [blame] | 174 | ```dart |
| 175 | // This URL is an endpoint that's provided by the authorization server. It's |
| 176 | // usually included in the server's documentation of its OAuth2 API. |
| 177 | final authorizationEndpoint = |
Nate Bosch | 1f4a79d | 2020-07-20 21:16:16 | [diff] [blame] | 178 | Uri.parse('http://example.com/oauth2/authorization'); |
Tobe Osakwe | 46d0f74 | 2019-11-19 22:43:13 | [diff] [blame] | 179 | |
| 180 | // The OAuth2 specification expects a client's identifier and secret |
| 181 | // to be sent when using the client credentials grant. |
| 182 | // |
| 183 | // Because the client credentials grant is not inherently associated with a user, |
| 184 | // it is up to the server in question whether the returned token allows limited |
| 185 | // API access. |
| 186 | // |
| 187 | // Either way, you must provide both a client identifier and a client secret: |
Nate Bosch | 1f4a79d | 2020-07-20 21:16:16 | [diff] [blame] | 188 | final identifier = 'my client identifier'; |
| 189 | final secret = 'my client secret'; |
Tobe Osakwe | 46d0f74 | 2019-11-19 22:43:13 | [diff] [blame] | 190 | |
| 191 | // Calling the top-level `clientCredentialsGrant` function will return a |
| 192 | // [Client] instead. |
| 193 | var client = await oauth2.clientCredentialsGrant( |
| 194 | authorizationEndpoint, identifier, secret); |
| 195 | |
| 196 | // With an authenticated client, you can make requests, and the `Bearer` token |
| 197 | // returned by the server during the client credentials grant will be attached |
| 198 | // to any request you make. |
Nate Bosch | 1f4a79d | 2020-07-20 21:16:16 | [diff] [blame] | 199 | var response = |
| 200 | await client.read('https://example.com/api/some_resource.json'); |
Tobe Osakwe | 46d0f74 | 2019-11-19 22:43:13 | [diff] [blame] | 201 | |
| 202 | // You can save the client's credentials, which consists of an access token, and |
| 203 | // potentially a refresh token and expiry date, to a file. This way, subsequent runs |
| 204 | // do not need to reauthenticate, and you can avoid saving the client identifier and |
| 205 | // secret. |
| 206 | await credentialsFile.writeAsString(client.credentials.toJson()); |
| 207 | ``` |
| 208 | |
Natalie Weizenbaum | 1e90128 | 2015-09-14 19:55:56 | [diff] [blame] | 209 | ## Resource Owner Password Grant |
Nate Bosch | 1f4a79d | 2020-07-20 21:16:16 | [diff] [blame] | 210 | |
| 211 | **Resources:** [Method summary][resourceOwnerPasswordGrantMethod], |
| 212 | [OAuth documentation][resourceOwnerPasswordGrantDocs] |
Erik Grimes | c658507 | 2015-09-14 19:55:18 | [diff] [blame] | 213 | |
Natalie Weizenbaum | 1e90128 | 2015-09-14 19:55:56 | [diff] [blame] | 214 | ```dart |
| 215 | // This URL is an endpoint that's provided by the authorization server. It's |
| 216 | // usually included in the server's documentation of its OAuth2 API. |
| 217 | final authorizationEndpoint = |
Nate Bosch | 1f4a79d | 2020-07-20 21:16:16 | [diff] [blame] | 218 | Uri.parse('http://example.com/oauth2/authorization'); |
Natalie Weizenbaum | 1e90128 | 2015-09-14 19:55:56 | [diff] [blame] | 219 | |
| 220 | // The user should supply their own username and password. |
Nate Bosch | 1f4a79d | 2020-07-20 21:16:16 | [diff] [blame] | 221 | final username = 'example user'; |
| 222 | final password = 'example password'; |
Natalie Weizenbaum | 1e90128 | 2015-09-14 19:55:56 | [diff] [blame] | 223 | |
| 224 | // The authorization server may issue each client a separate client |
| 225 | // identifier and secret, which allows the server to tell which client |
| 226 | // is accessing it. Some servers may also have an anonymous |
| 227 | // identifier/secret pair that any client may use. |
| 228 | // |
| 229 | // Some servers don't require the client to authenticate itself, in which case |
| 230 | // these should be omitted. |
Nate Bosch | 1f4a79d | 2020-07-20 21:16:16 | [diff] [blame] | 231 | final identifier = 'my client identifier'; |
| 232 | final secret = 'my client secret'; |
Natalie Weizenbaum | 1e90128 | 2015-09-14 19:55:56 | [diff] [blame] | 233 | |
| 234 | // Make a request to the authorization endpoint that will produce the fully |
| 235 | // authenticated Client. |
| 236 | var client = await oauth2.resourceOwnerPasswordGrant( |
| 237 | authorizationEndpoint, username, password, |
| 238 | identifier: identifier, secret: secret); |
| 239 | |
| 240 | // Once you have the client, you can use it just like any other HTTP client. |
Nate Bosch | 1f4a79d | 2020-07-20 21:16:16 | [diff] [blame] | 241 | var result = await client.read('http://example.com/protected-resources.txt'); |
Natalie Weizenbaum | 1e90128 | 2015-09-14 19:55:56 | [diff] [blame] | 242 | |
| 243 | // Once we're done with the client, save the credentials file. This will allow |
| 244 | // us to re-use the credentials and avoid storing the username and password |
| 245 | // directly. |
Nate Bosch | 1f4a79d | 2020-07-20 21:16:16 | [diff] [blame] | 246 | File('~/.myapp/credentials.json').writeAsString(client.credentials.toJson()); |
Erik Grimes | c658507 | 2015-09-14 19:55:18 | [diff] [blame] | 247 | ``` |
Levi Hassel | 6649c87 | 2020-04-27 09:12:26 | [diff] [blame] | 248 | |
| 249 | [authorizationCodeGrantDocs]: https://oauth.net/2/grant-types/authorization-code/ |
| 250 | [authorizationCodeGrantMethod]: https://pub.dev/documentation/oauth2/latest/oauth2/AuthorizationCodeGrant-class.html |
| 251 | [authorizationCodeGrantSection]: #authorization-code-grant |
| 252 | [clientCredentialsGrantDocs]: https://oauth.net/2/grant-types/client-credentials/ |
| 253 | [clientCredentialsGrantMethod]: https://pub.dev/documentation/oauth2/latest/oauth2/clientCredentialsGrant.html |
| 254 | [clientCredentialsGrantSection]: #client-credentials-grant |
| 255 | [resourceOwnerPasswordGrantDocs]: https://oauth.net/2/grant-types/password/ |
| 256 | [resourceOwnerPasswordGrantMethod]: https://pub.dev/documentation/oauth2/latest/oauth2/resourceOwnerPasswordGrant.html |
| 257 | [resourceOwnerPasswordGrantSection]: #resource-owner-password-grant |
| 258 | [uni_links]: https://pub.dev/packages/uni_links |
| 259 | [url_launcher]: https://pub.dev/packages/url_launcher |
| 260 | [webview_flutter]: https://pub.dev/packages/webview_flutter |