-
Notifications
You must be signed in to change notification settings - Fork 94
feat: add support for OAuth 2.0 discovery, registration, resource indicators, and custom auth #2338
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 3 commits
e4d905b
8ef3bae
dde8900
e677291
14a8598
9a3caca
a350fd6
20dcbb4
7c92a10
2922351
43496a3
256511e
871863d
0b8cc35
c48ab45
912af44
92776cc
86adda1
75c2f99
cc61861
219d352
bddf7df
907b0f6
6d30cb1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| // Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file | ||
| // for details. All rights reserved. Use of this source code is governed by a | ||
| // BSD-style license that can be found in the LICENSE file. | ||
|
|
||
| import 'dart:async'; | ||
|
|
||
| /// A callback used to add additional client authentication headers or body | ||
| /// parameters to a token request (e.g., for JWT assertions per RFC 7523). | ||
| typedef ClientAuthenticator = FutureOr<void> Function( | ||
| Map<String, String> headers, Map<String, String> body); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,7 @@ import 'package:http/http.dart' as http; | |
| import 'package:http_parser/http_parser.dart'; | ||
|
|
||
| import 'client.dart'; | ||
| import 'client_authenticator.dart'; | ||
| import 'handle_access_token_response.dart'; | ||
| import 'utils.dart'; | ||
|
|
||
|
|
@@ -45,6 +46,8 @@ Future<Client> clientCredentialsGrant( | |
| bool basicAuth = true, | ||
| http.Client? httpClient, | ||
| String? delimiter, | ||
| ClientAuthenticator? customAuth, | ||
| Iterable<Uri>? resources, | ||
| Map<String, dynamic> Function(MediaType? contentType, String body)? | ||
| getParameters}) async { | ||
|
Comment on lines
50
to
59
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The new References
|
||
| delimiter ??= ' '; | ||
|
|
@@ -54,7 +57,10 @@ Future<Client> clientCredentialsGrant( | |
|
|
||
| var headers = <String, String>{}; | ||
|
|
||
| if (identifier != null) { | ||
| if (customAuth != null) { | ||
| if (identifier != null) body['client_id'] = identifier; | ||
| await customAuth(headers, body); | ||
| } else if (identifier != null) { | ||
| if (basicAuth) { | ||
| headers['Authorization'] = basicAuthHeader(identifier, secret!); | ||
| } else { | ||
|
|
@@ -67,6 +73,34 @@ Future<Client> clientCredentialsGrant( | |
| body['scope'] = scopes.join(delimiter); | ||
| } | ||
|
|
||
| if (resources != null && resources.isNotEmpty) { | ||
| // http.post doesn't support Map<String, Iterable> for x-www-form-urlencoded | ||
| // bodies, so we construct the body string manually to allow | ||
| // multiple 'resource' parameters per RFC 8707. | ||
| final encodedBody = body.entries | ||
| .map((e) => '${Uri.encodeQueryComponent(e.key)}=' | ||
| '${Uri.encodeQueryComponent(e.value)}') | ||
| .toList(); | ||
| for (final r in resources) { | ||
| encodedBody.add('resource=${Uri.encodeQueryComponent(r.toString())}'); | ||
| } | ||
|
|
||
| httpClient ??= http.Client(); | ||
| var response = await httpClient.post(authorizationEndpoint, | ||
| headers: headers | ||
| ..['content-type'] = 'application/x-www-form-urlencoded', | ||
| body: encodedBody.join('&')); | ||
|
|
||
| var credentials = handleAccessTokenResponse(response, authorizationEndpoint, | ||
| startTime, scopes?.toList() ?? [], delimiter, | ||
| getParameters: getParameters); | ||
| return Client(credentials, | ||
| identifier: identifier, | ||
| secret: secret, | ||
| httpClient: httpClient, | ||
| customAuth: customAuth); | ||
| } | ||
|
Comment on lines
+83
to
+109
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This Please consider refactoring to remove the duplication. You could, for instance, determine the request body and content-type first, and then have a single path for making the request and processing the response. |
||
|
|
||
| httpClient ??= http.Client(); | ||
| var response = await httpClient.post(authorizationEndpoint, | ||
| headers: headers, body: body); | ||
|
|
@@ -75,5 +109,8 @@ Future<Client> clientCredentialsGrant( | |
| startTime, scopes?.toList() ?? [], delimiter, | ||
| getParameters: getParameters); | ||
| return Client(credentials, | ||
| identifier: identifier, secret: secret, httpClient: httpClient); | ||
| identifier: identifier, | ||
| secret: secret, | ||
| httpClient: httpClient, | ||
| customAuth: customAuth); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,6 +9,7 @@ import 'dart:convert'; | |
| import 'package:http/http.dart' as http; | ||
| import 'package:http_parser/http_parser.dart'; | ||
|
|
||
| import 'client_authenticator.dart'; | ||
| import 'handle_access_token_response.dart'; | ||
| import 'parameters.dart'; | ||
| import 'utils.dart'; | ||
|
|
@@ -216,6 +217,8 @@ class Credentials { | |
| String? secret, | ||
| Iterable<String>? newScopes, | ||
| bool basicAuth = true, | ||
| ClientAuthenticator? customAuth, | ||
| Iterable<Uri>? resources, | ||
| http.Client? httpClient}) async { | ||
| var scopes = this.scopes; | ||
| if (newScopes != null) scopes = newScopes.toList(); | ||
|
|
@@ -238,16 +241,44 @@ class Credentials { | |
|
|
||
| var headers = <String, String>{}; | ||
|
|
||
| var body = {'grant_type': 'refresh_token', 'refresh_token': refreshToken}; | ||
| var body = <String, String>{ | ||
| 'grant_type': 'refresh_token', | ||
| 'refresh_token': refreshToken!, | ||
| }; | ||
| if (scopes.isNotEmpty) body['scope'] = scopes.join(_delimiter); | ||
|
|
||
| if (basicAuth && secret != null) { | ||
| if (customAuth != null) { | ||
| if (identifier != null) body['client_id'] = identifier; | ||
| await customAuth(headers, body); | ||
| } else if (basicAuth && secret != null) { | ||
| headers['Authorization'] = basicAuthHeader(identifier!, secret); | ||
| } else { | ||
| if (identifier != null) body['client_id'] = identifier; | ||
| if (secret != null) body['client_secret'] = secret; | ||
| } | ||
|
|
||
| if (resources != null && resources.isNotEmpty) { | ||
| final encodedBody = body.entries | ||
| .map((e) => '${Uri.encodeQueryComponent(e.key)}=' | ||
| '${Uri.encodeQueryComponent(e.value)}') | ||
| .toList(); | ||
| for (final r in resources) { | ||
| encodedBody.add('resource=${Uri.encodeQueryComponent(r.toString())}'); | ||
| } | ||
| headers['content-type'] = 'application/x-www-form-urlencoded'; | ||
| var response = await httpClient.post(tokenEndpoint, | ||
| headers: headers, body: encodedBody.join('&')); | ||
| var credentials = handleAccessTokenResponse( | ||
| response, tokenEndpoint, startTime, scopes, _delimiter, | ||
| getParameters: _getParameters); | ||
| if (credentials.refreshToken != null) return credentials; | ||
| return Credentials(credentials.accessToken, | ||
| refreshToken: refreshToken, | ||
| idToken: credentials.idToken, | ||
| tokenEndpoint: credentials.tokenEndpoint, | ||
| scopes: credentials.scopes, | ||
| expiration: credentials.expiration); | ||
| } | ||
|
Comment on lines
+266
to
+287
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is significant code duplication between this block that handles requests with Consider refactoring to unify the logic. For example, you could prepare the request body (either as a
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But is this code really that complicated.. we're implementing a spec, this won't be touched much. And abstractions might just make everything weirder.. Straight lines of code is awesome.. too many fancy private methods trying to share trivial logic doesn't make the world better. |
||
|
|
||
| var response = | ||
| await httpClient.post(tokenEndpoint, headers: headers, body: body); | ||
| var credentials = handleAccessTokenResponse( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new
resourcesparameter ingetAuthorizationUrlis not documented. According to the repository's style guide, all public members should be documented. Please add documentation for this new parameter to explain its purpose, which is for RFC 8707 Resource Indicators.References