Skip to content
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

[googleapis_auth] Add support for non-Google OAuth providers #597

Merged
merged 27 commits into from
Feb 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
efb93f5
Port shorebird fork
bryanoltman Feb 27, 2024
c7de4ec
revert function deletion
bryanoltman Feb 27, 2024
3de0e8d
reset imports, remove auth endpoints where unnecessary
bryanoltman Feb 27, 2024
aa3709d
fix import
bryanoltman Feb 27, 2024
000ab1f
fix imports
bryanoltman Feb 27, 2024
4c531c3
add <void> to drain
bryanoltman Feb 27, 2024
34a0cae
cleanup
bryanoltman Feb 27, 2024
292dd50
fix auth_code.dart
bryanoltman Feb 27, 2024
4ada5ae
revert token model to upstream
bryanoltman Feb 27, 2024
22fe012
revert to upstream
bryanoltman Feb 27, 2024
8b6967e
revert to upstream
bryanoltman Feb 27, 2024
2bb624f
clean up jwt.dart
bryanoltman Feb 27, 2024
fc314a9
comments
bryanoltman Feb 27, 2024
b814d68
provide default value for auth endpoints
bryanoltman Feb 27, 2024
bb18b23
provide default value for auth endpoints
bryanoltman Feb 27, 2024
0f57c6c
reset imports
bryanoltman Feb 27, 2024
830ca5d
revert drain to upstream
bryanoltman Feb 27, 2024
c26f4a2
tidy tests
bryanoltman Feb 27, 2024
d617842
lints
bryanoltman Feb 27, 2024
80cd1f8
add tests
bryanoltman Feb 27, 2024
87a1d29
delete irrelevant test
bryanoltman Feb 27, 2024
6544dba
update client secret requirement
bryanoltman Feb 27, 2024
eee5d57
update changelog and readme
bryanoltman Feb 27, 2024
1015535
readme tweak
bryanoltman Feb 27, 2024
90a1693
update pubspec.yaml version
bryanoltman Feb 27, 2024
f57f9c3
provide default AuthEndpoints value for createAuthenticationUri
bryanoltman Feb 27, 2024
4369c78
remove unused test function
bryanoltman Feb 27, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions googleapis_auth/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 1.5.0

- Add support for non-Google OAuth 2.0 providers.

## 1.4.2

- Require Dart 3.2 or later.
Expand Down
32 changes: 32 additions & 0 deletions googleapis_auth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,38 @@ var client = clientViaApiKey('<api-key-from-devconsole>');
client.close();
```

### Using a non-Google authentication provider

This package is designed to work with Google's OAuth flow, but it can be used
with other OAuth providers as well. To do this, you need to subclass
`AuthEndpoints` and provide authorization and token uris. For example:

```dart
import 'package:googleapis_auth/auth_io.dart';

class MicrosoftAuthEndpoints extends AuthEndpoints {
@override
Uri get authorizationEndpoint =>
Uri.https('login.microsoftonline.com', 'common/oauth2/v2.0/authorize');

@override
Uri get tokenEndpoint =>
Uri.https('login.microsoftonline.com', 'common/oauth2/v2.0/token');
}
```

This can then be used to obtain credentials:

```dart
final credentials = await obtainAccessCredentialsViaUserConsent(
clientId,
['scope1', 'scope2'],
client,
prompt,
authEndpoints: MicrosoftAuthEndpoints(),
);
```

### More information

More information can be obtained from official Google Developers documentation:
Expand Down
11 changes: 11 additions & 0 deletions googleapis_auth/lib/auth_io.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'dart:io';
import 'package:http/http.dart';

import 'src/adc_utils.dart';
import 'src/auth_endpoints.dart';
import 'src/auth_http_utils.dart';
import 'src/http_client_base.dart';
import 'src/metadata_server_client.dart' show clientViaMetadataServer;
Expand Down Expand Up @@ -122,6 +123,7 @@ Future<AutoRefreshingAuthClient> clientViaUserConsent(
Client? baseClient,
String? hostedDomain,
int listenPort = 0,
AuthEndpoints authEndpoints = const GoogleAuthEndpoints(),
}) async {
var closeUnderlyingClient = false;
if (baseClient == null) {
Expand All @@ -130,6 +132,7 @@ Future<AutoRefreshingAuthClient> clientViaUserConsent(
}

final flow = AuthorizationCodeGrantServerFlow(
authEndpoints,
clientId,
scopes,
baseClient,
Expand All @@ -150,6 +153,7 @@ Future<AutoRefreshingAuthClient> clientViaUserConsent(
}
return AutoRefreshingClient(
baseClient,
authEndpoints,
clientId,
credentials,
closeUnderlyingClient: closeUnderlyingClient,
Expand Down Expand Up @@ -177,6 +181,7 @@ Future<AutoRefreshingAuthClient> clientViaUserConsentManual(
PromptUserForConsentManual userPrompt, {
Client? baseClient,
String? hostedDomain,
AuthEndpoints authEndpoints = const GoogleAuthEndpoints(),
}) async {
var closeUnderlyingClient = false;
if (baseClient == null) {
Expand All @@ -185,6 +190,7 @@ Future<AutoRefreshingAuthClient> clientViaUserConsentManual(
}

final flow = AuthorizationCodeGrantManualFlow(
authEndpoints,
clientId,
scopes,
baseClient,
Expand All @@ -205,6 +211,7 @@ Future<AutoRefreshingAuthClient> clientViaUserConsentManual(

return AutoRefreshingClient(
baseClient,
authEndpoints,
clientId,
credentials,
closeUnderlyingClient: closeUnderlyingClient,
Expand Down Expand Up @@ -238,8 +245,10 @@ Future<AccessCredentials> obtainAccessCredentialsViaUserConsent(
PromptUserForConsent userPrompt, {
String? hostedDomain,
int listenPort = 0,
AuthEndpoints authEndpoints = const GoogleAuthEndpoints(),
}) =>
AuthorizationCodeGrantServerFlow(
authEndpoints,
clientId,
scopes,
client,
Expand All @@ -266,8 +275,10 @@ Future<AccessCredentials> obtainAccessCredentialsViaUserConsentManual(
Client client,
PromptUserForConsentManual userPrompt, {
String? hostedDomain,
AuthEndpoints authEndpoints = const GoogleAuthEndpoints(),
}) =>
AuthorizationCodeGrantManualFlow(
authEndpoints,
clientId,
scopes,
client,
Expand Down
1 change: 1 addition & 0 deletions googleapis_auth/lib/googleapis_auth.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
library googleapis_auth;

export 'src/auth_client.dart';
export 'src/auth_endpoints.dart';
export 'src/auth_functions.dart';
export 'src/client_id.dart';
export 'src/exceptions.dart';
Expand Down
2 changes: 2 additions & 0 deletions googleapis_auth/lib/src/adc_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'dart:io';

import 'package:http/http.dart';

import 'auth_endpoints.dart';
import 'auth_functions.dart';
import 'auth_http_utils.dart';
import 'service_account_client.dart';
Expand Down Expand Up @@ -39,6 +40,7 @@ Future<AutoRefreshingAuthClient> fromApplicationsCredentialsFile(
);
return AutoRefreshingClient(
baseClient,
const GoogleAuthEndpoints(),
clientId,
await refreshCredentials(
clientId,
Expand Down
34 changes: 34 additions & 0 deletions googleapis_auth/lib/src/auth_endpoints.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import 'known_uris.dart';

/// {@template AuthEndpoints}
/// The endpoints required for an OAuth 2.0 authorization code flow.
/// {@endtemplate}
abstract class AuthEndpoints {
/// {@macro AuthEndpoints}
const AuthEndpoints();

/// The OAuth endpoint used to obtain a single-use authorization code.
Uri get authorizationEndpoint;

/// The OAuth endpoint used to exchange an authorization code for access
/// credentials.
Uri get tokenEndpoint;
}

/// {@template GoogleAuthEndpoints}
/// The endpoints required for an OAuth 2.0 authorization code flow with Google.
///
/// This is the only implementation of [AuthEndpoints] provided by this package.
/// Package consumers can provide their own implementation if they are using a
/// different OAuth 2.0 provider or providers.
/// {@endtemplate}
class GoogleAuthEndpoints extends AuthEndpoints {
/// {@macro GoogleAuthEndpoints}
const GoogleAuthEndpoints();

@override
Uri get authorizationEndpoint => googleOauth2AuthorizationEndpoint;

@override
Uri get tokenEndpoint => googleOauth2TokenEndpoint;
}
35 changes: 19 additions & 16 deletions googleapis_auth/lib/src/auth_functions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:http/http.dart';

import 'access_credentials.dart';
import 'auth_client.dart';
import 'auth_endpoints.dart';
import 'auth_http_utils.dart';
import 'client_id.dart';
import 'http_client_base.dart';
Expand Down Expand Up @@ -84,15 +85,16 @@ AuthClient authenticatedClient(
AutoRefreshingAuthClient autoRefreshingClient(
ClientId clientId,
AccessCredentials credentials,
Client baseClient,
) {
Client baseClient, {
AuthEndpoints authEndpoints = const GoogleAuthEndpoints(),
}) {
if (credentials.accessToken.type != 'Bearer') {
throw ArgumentError('Only Bearer access tokens are accepted.');
}
if (credentials.refreshToken == null) {
throw ArgumentError('Refresh token in AccessCredentials was `null`.');
}
return AutoRefreshingClient(baseClient, clientId, credentials);
return AutoRefreshingClient(baseClient, authEndpoints, clientId, credentials);
}

/// Obtains refreshed [AccessCredentials] for [clientId] and [credentials].
Expand All @@ -103,25 +105,26 @@ AutoRefreshingAuthClient autoRefreshingClient(
Future<AccessCredentials> refreshCredentials(
ClientId clientId,
AccessCredentials credentials,
Client client,
) async {
final secret = clientId.secret;
if (secret == null) {
throw ArgumentError('clientId.secret cannot be null.');
}

Client client, {
AuthEndpoints authEndpoints = const GoogleAuthEndpoints(),
}) async {
final refreshToken = credentials.refreshToken;
if (refreshToken == null) {
throw ArgumentError('clientId.refreshToken cannot be null.');
}

// https://developers.google.com/identity/protocols/oauth2/native-app#offline
final jsonMap = await client.oauthTokenRequest({
'client_id': clientId.identifier,
'client_secret': secret,
'refresh_token': refreshToken,
'grant_type': 'refresh_token',
});
final jsonMap = await client.oauthTokenRequest(
{
'client_id': clientId.identifier,
// Not all providers require a client secret,
// e.g. https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow#refresh-the-access-token
if (clientId.secret != null) 'client_secret': clientId.secret!,
'refresh_token': refreshToken,
'grant_type': 'refresh_token',
},
authEndpoints: authEndpoints,
);

final accessToken = parseAccessToken(jsonMap);

Expand Down
10 changes: 9 additions & 1 deletion googleapis_auth/lib/src/auth_http_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:http/http.dart';

import 'access_credentials.dart';
import 'auth_client.dart';
import 'auth_endpoints.dart';
import 'auth_functions.dart';
import 'client_id.dart';
import 'exceptions.dart';
Expand Down Expand Up @@ -91,9 +92,11 @@ class AutoRefreshingClient extends AutoRefreshDelegatingClient {
@override
AccessCredentials credentials;
late Client authClient;
final AuthEndpoints authEndpoints;

AutoRefreshingClient(
super.client,
this.authEndpoints,
this.clientId,
this.credentials, {
super.closeUnderlyingClient,
Expand All @@ -114,7 +117,12 @@ class AutoRefreshingClient extends AutoRefreshDelegatingClient {
// If so, we should handle it.
return authClient.send(request);
} else {
final cred = await refreshCredentials(clientId, credentials, baseClient);
final cred = await refreshCredentials(
clientId,
credentials,
baseClient,
authEndpoints: authEndpoints,
);
notifyAboutNewCredentials(cred);
credentials = cred;
authClient = AuthenticatedClient(
Expand Down
7 changes: 5 additions & 2 deletions googleapis_auth/lib/src/oauth2_flows/auth_code.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ import 'package:crypto/crypto.dart';
import 'package:http/http.dart' as http;

import '../access_credentials.dart';
import '../auth_endpoints.dart';
import '../client_id.dart';
import '../exceptions.dart';
import '../known_uris.dart';
import '../utils.dart';

Uri createAuthenticationUri({
Expand All @@ -24,6 +24,7 @@ Uri createAuthenticationUri({
String? hostedDomain,
String? state,
bool offline = false,
AuthEndpoints authEndpoints = const GoogleAuthEndpoints(),
}) {
final queryValues = {
'client_id': clientId,
Expand All @@ -36,7 +37,7 @@ Uri createAuthenticationUri({
if (hostedDomain != null) 'hd': hostedDomain,
if (state != null) 'state': state,
};
return googleOauth2AuthorizationEndpoint.replace(
return authEndpoints.authorizationEndpoint.replace(
queryParameters: queryValues,
);
}
Expand Down Expand Up @@ -111,6 +112,7 @@ Future<AccessCredentials> obtainAccessCredentialsViaCodeExchange(
String code, {
String redirectUrl = 'postmessage',
String? codeVerifier,
AuthEndpoints authEndpoints = const GoogleAuthEndpoints(),
}) async {
final jsonMap = await client.oauthTokenRequest(
{
Expand All @@ -121,6 +123,7 @@ Future<AccessCredentials> obtainAccessCredentialsViaCodeExchange(
'grant_type': 'authorization_code',
'redirect_uri': redirectUrl,
},
authEndpoints: authEndpoints,
);
final accessToken = parseAccessToken(jsonMap);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,20 @@
import 'package:http/http.dart' as http;

import '../access_credentials.dart';
import '../auth_endpoints.dart';
import '../client_id.dart';
import 'auth_code.dart';
import 'base_flow.dart';

abstract class AuthorizationCodeGrantAbstractFlow implements BaseFlow {
final AuthEndpoints authEndpoints;
final ClientId clientId;
final String? hostedDomain;
final List<String> scopes;
final http.Client _client;

AuthorizationCodeGrantAbstractFlow(
this.authEndpoints,
this.clientId,
this.scopes,
this._client, {
Expand All @@ -25,6 +28,7 @@ abstract class AuthorizationCodeGrantAbstractFlow implements BaseFlow {
Future<AccessCredentials> obtainAccessCredentialsUsingCodeImpl(
String code,
String redirectUri, {
required AuthEndpoints authEndpoints,
required String codeVerifier,
}) =>
obtainAccessCredentialsViaCodeExchange(
Expand All @@ -33,6 +37,7 @@ abstract class AuthorizationCodeGrantAbstractFlow implements BaseFlow {
code,
redirectUrl: redirectUri,
codeVerifier: codeVerifier,
authEndpoints: authEndpoints,
);

Uri authenticationUri(
Expand All @@ -41,6 +46,7 @@ abstract class AuthorizationCodeGrantAbstractFlow implements BaseFlow {
required String codeVerifier,
}) =>
createAuthenticationUri(
authEndpoints: authEndpoints,
redirectUri: redirectUri,
clientId: clientId.identifier,
scopes: scopes,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class AuthorizationCodeGrantManualFlow
final PromptUserForConsentManual userPrompt;

AuthorizationCodeGrantManualFlow(
super.authEndpoints,
super.clientId,
super.scopes,
super.client,
Expand All @@ -47,6 +48,7 @@ class AuthorizationCodeGrantManualFlow
return obtainAccessCredentialsUsingCodeImpl(
code,
_redirectionUri,
authEndpoints: authEndpoints,
codeVerifier: codeVerifier,
);
}
Expand Down
Loading
Loading