Skip to content

Commit

Permalink
Support discovery
Browse files Browse the repository at this point in the history
  • Loading branch information
Rexios80 committed Aug 19, 2024
1 parent cc4aa84 commit 09ad8c8
Show file tree
Hide file tree
Showing 7 changed files with 91 additions and 21 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.0.3

- Supports discovery

## 0.0.2

- Adds token revocation support
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ A complete OAuth2 solution for Flutter apps. Handles auth, token storage, and to
- Refresh token expiration handler
- Nonce, PKCE, and state verification
- OIDC support
- Endpoint discovery
- Access to the ID token and raw nonce
- Works with Firebase OIDC implicit flow

Expand Down Expand Up @@ -110,9 +111,9 @@ void main() async {
// The `baseUrl` is the OAuth `aud` parameter
dio: Dio(BaseOptions(baseUrl: 'https://api.fitbit.com/1/user')),
endpoints: OAuth2Endpoints(
authorize: 'https://fitbit.com/oauth2/authorize',
authorization: 'https://fitbit.com/oauth2/authorize',
token: 'https://api.fitbit.com/oauth2/token',
revoke: 'https://api.fitbit.com/oauth2/revoke',
revocation: 'https://api.fitbit.com/oauth2/revoke',
),
// Use `OAuth2Endpoints.base` for services with a consistent base URL
// endpoints: OAuth2Endpoints.base('https://api.fitbit.com/oauth2'),
Expand Down
4 changes: 2 additions & 2 deletions example/example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ void main() async {
// The `baseUrl` is the OAuth `aud` parameter
dio: Dio(BaseOptions(baseUrl: 'https://api.fitbit.com/1/user')),
endpoints: OAuth2Endpoints(
authorize: 'https://fitbit.com/oauth2/authorize',
authorization: 'https://fitbit.com/oauth2/authorize',
token: 'https://api.fitbit.com/oauth2/token',
revoke: 'https://api.fitbit.com/oauth2/revoke',
revocation: 'https://api.fitbit.com/oauth2/revoke',
),
// Use `OAuth2Endpoints.base` for services with a consistent base URL
// endpoints: OAuth2Endpoints.base('https://api.fitbit.com/oauth2'),
Expand Down
31 changes: 23 additions & 8 deletions lib/src/model/oauth2_endpoints.dart
Original file line number Diff line number Diff line change
@@ -1,30 +1,45 @@
import 'package:json_annotation/json_annotation.dart';

part 'oauth2_endpoints.g.dart';

/// Wrapper for all OAuth2 endpoints
@JsonSerializable()
class OAuth2Endpoints {
/// URI for authorization
final Uri authorize;
@JsonKey(name: 'authorization_endpoint')
final Uri authorization;

/// URI for token exchange
@JsonKey(name: 'token_endpoint')
final Uri token;

/// URI for token revocation
final Uri revoke;
@JsonKey(name: 'revocation_endpoint')
final Uri? revocation;

/// Construct with explicit URLs
OAuth2Endpoints({
required String authorize,
required String authorization,
required String token,
required String revoke,
}) : authorize = Uri.parse(authorize),
String? revocation,
}) : authorization = Uri.parse(authorization),
token = Uri.parse(token),
revoke = Uri.parse(revoke);
revocation = revocation != null ? Uri.parse(revocation) : null;

/// Construct with a base URL
///
/// Convenient for services that have a consistent base URL
OAuth2Endpoints.base(String base)
: this(
authorize: '$base/authorize',
authorization: '$base/authorize',
token: '$base/token',
revoke: '$base/revoke',
revocation: '$base/revoke',
);

/// From json
factory OAuth2Endpoints.fromJson(Map<String, dynamic> json) =>
_$OAuth2EndpointsFromJson(json);

/// To json
Map<String, dynamic> toJson() => _$OAuth2EndpointsToJson(this);
}
21 changes: 21 additions & 0 deletions lib/src/model/oauth2_endpoints.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

45 changes: 37 additions & 8 deletions lib/src/oauth_flutter_base.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:convert';

import 'package:dio/dio.dart';
Expand Down Expand Up @@ -31,6 +32,11 @@ class OAuth2Client<T extends SecureOAuth2Token> {
static const _uuid = Uuid();
static const _keyPrefix = '_oauth_flutter_token_';

/// Completer for the OAuth2 endpoints
///
/// Either completed in the constructor or when discovery is performed
final _endpoints = Completer<OAuth2Endpoints>();

/// Access to the OAuth dio client
///
/// Must include the base path for API operations. This is the OAuth audience.
Expand All @@ -39,8 +45,8 @@ class OAuth2Client<T extends SecureOAuth2Token> {
/// The client to use for OAuth operations
final Dio oauthDio;

/// The OAuth2 endpoints
final OAuth2Endpoints endpoints;
/// The discovery URI
final Uri? discoveryUri;

/// The redirect URI
final Uri redirectUri;
Expand Down Expand Up @@ -79,12 +85,15 @@ class OAuth2Client<T extends SecureOAuth2Token> {
/// The token refresher
late final Fresh fresh;

/// Constructor
/// Create an OAuth2 client
///
/// One of [endpoints] or [discoveryUri] must be provided
OAuth2Client({
required String key,
required this.dio,
Dio? oauthDio,
required this.endpoints,
OAuth2Endpoints? endpoints,
this.discoveryUri,
required this.redirectUri,
this.callbackUrlScheme = 'https',
this.credentials,
Expand All @@ -93,9 +102,13 @@ class OAuth2Client<T extends SecureOAuth2Token> {
ReAuthenticationCallback<T>? onReAuthenticate,
this.redirectOriginOverride,
this.verification = const OAuth2Verification(),
}) : tokenDecoder =
}) : assert((endpoints != null) ^ (discoveryUri != null)),
tokenDecoder =
tokenDecoder ?? SecureOAuth2Token.fromJson as OAuth2TokenDecoder<T>,
oauthDio = oauthDio ?? Dio() {
if (endpoints != null) {
_endpoints.complete(endpoints);
}
fresh = Fresh.oAuth2(
tokenStorage: SecureTokenStorage(
key: '$_keyPrefix$key',
Expand All @@ -109,6 +122,14 @@ class OAuth2Client<T extends SecureOAuth2Token> {
dio.interceptors.add(fresh);
}

Future<OAuth2Endpoints> _discover() async {
if (_endpoints.isCompleted) return _endpoints.future;
final response = await oauthDio.getUri(discoveryUri!);
final endpoints = OAuth2Endpoints.fromJson(response.data);
_endpoints.complete(endpoints);
return endpoints;
}

Future<T> _refreshToken({
required T? oldToken,
required ReAuthenticationCallback<T> onReAuthenticate,
Expand Down Expand Up @@ -147,9 +168,10 @@ class OAuth2Client<T extends SecureOAuth2Token> {
final state = _uuid.v4();
final pkce = PkcePair.generate();

final endpoints = await _discover();
final credentials = this.credentials;
final uri = endpoints.authorize.replace(
path: endpoints.authorize.path,
final uri = endpoints.authorization.replace(
path: endpoints.authorization.path,
queryParameters: {
if (credentials != null) 'client_id': credentials.id,
'response_type': 'code',
Expand Down Expand Up @@ -185,6 +207,7 @@ class OAuth2Client<T extends SecureOAuth2Token> {
Future<T> token({
required OAuthAuthorization authorization,
}) async {
final endpoints = await _discover();
final credentials = this.credentials;
final response = await oauthDio.postUri(
endpoints.token,
Expand Down Expand Up @@ -217,6 +240,7 @@ class OAuth2Client<T extends SecureOAuth2Token> {
Future<T> refresh({
required T token,
}) async {
final endpoints = await _discover();
final response = await oauthDio.postUri(
endpoints.token,
options: Options(headers: _tokenHeaders),
Expand All @@ -240,9 +264,14 @@ class OAuth2Client<T extends SecureOAuth2Token> {
final token = await fresh.token as T?;
if (token == null) return;

final endpoints = await _discover();
final revocation = endpoints.revocation;
if (revocation == null) {
throw Exception('Revocation endpoint not available');
}
final credentials = this.credentials;
final response = await oauthDio.postUri(
endpoints.revoke,
revocation,
options: Options(headers: _tokenHeaders),
data: {
if (credentials != null) 'client_id': credentials.id,
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: oauth_flutter
description: "A complete OAuth2 solution for Flutter apps. Handles auth, token storage, and token refresh."
version: 0.0.2
version: 0.0.3
homepage: https://github.com/IO-Design-Team/oauth_flutter

environment:
Expand Down

0 comments on commit 09ad8c8

Please sign in to comment.