From 00db5cd02a49e839d3876c567fe7d22e28b3019a Mon Sep 17 00:00:00 2001 From: IamPekka058 <59747867+IamPekka058@users.noreply.github.com> Date: Thu, 31 Jul 2025 20:27:00 +0200 Subject: [PATCH 1/3] :sparkles: Implement OAuth2 PKCE flow with authorization URL generation and token exchange --- lib/src/common/util/oauth2_pkce.dart | 98 ++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 lib/src/common/util/oauth2_pkce.dart diff --git a/lib/src/common/util/oauth2_pkce.dart b/lib/src/common/util/oauth2_pkce.dart new file mode 100644 index 00000000..51e05acd --- /dev/null +++ b/lib/src/common/util/oauth2_pkce.dart @@ -0,0 +1,98 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:github_flutter/src/common.dart'; +import 'package:http/http.dart' as http; + +class OAuth2PKCE { + /// OAuth2 Client ID + final String clientId; + + /// Requested Scopes + final List scopes; + + /// Redirect URI + final String? redirectUri; + + /// State + final String? state; + + /// OAuth2 Base URL + final String baseUrl; + + /// Confidential Intermediary Server URL + final String confidentialIntermediaryServer; + + GitHub? github; + + /// Constructor to initialize [OAuth2PKCE] with required parameters. + OAuth2PKCE( + this.clientId, + this.confidentialIntermediaryServer, { + String? redirectUri, + this.scopes = const [], + this.state, + this.github, + this.baseUrl = 'https://github.com/login/oauth', + }) : redirectUri = + redirectUri == null ? null : _checkRedirectUri(redirectUri); + + static String _checkRedirectUri(String uri) { + return uri.contains('?') ? uri.substring(0, uri.indexOf('?')) : uri; + } + + /// Generates an Authorization URL + /// + /// This should be displayed to the user. + String createAuthorizeUrl() { + return '$baseUrl/authorize${buildQueryString({'client_id': clientId, 'scope': scopes.join(','), 'redirect_uri': redirectUri, 'state': state})}'; + } + + Future exchange( + String code, + String codeVerifier, [ + String? origin, + ]) async { + final headers = { + 'Accept': 'application/json', + 'content-type': 'application/json', + }; + if (origin != null) { + headers['Origin'] = origin; + } + + final body = GitHubJson.encode({ + 'client_id': clientId, + 'code_verifier': codeVerifier, + 'code': code, + 'redirect_uri': redirectUri, + }); + + return (github == null ? http.Client() : github!.client) + .post( + Uri.parse(confidentialIntermediaryServer), + body: body, + headers: headers, + ) + .then((response) { + final json = jsonDecode(response.body) as Map; + if (json['error'] != null) { + throw Exception(json['error']); + } + return ExchangeResponse( + json['access_token'], + json['token_type'], + (json['scope'] as String).split(','), + ); + }); + } +} + +/// Represents a response for exchanging a code for a token. +class ExchangeResponse { + final String? token; + final List scopes; + final String? tokenType; + + ExchangeResponse(this.token, this.tokenType, this.scopes); +} From 6553e9a0eb3a6b771300966d761f89c0367d686d Mon Sep 17 00:00:00 2001 From: Florian Barth Date: Fri, 1 Aug 2025 23:28:45 +0200 Subject: [PATCH 2/3] :sparkles: Update createAuthorizeUrl to include code challenge for PKCE flow --- lib/src/common/util/oauth2_pkce.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/common/util/oauth2_pkce.dart b/lib/src/common/util/oauth2_pkce.dart index 51e05acd..dd82d924 100644 --- a/lib/src/common/util/oauth2_pkce.dart +++ b/lib/src/common/util/oauth2_pkce.dart @@ -44,8 +44,8 @@ class OAuth2PKCE { /// Generates an Authorization URL /// /// This should be displayed to the user. - String createAuthorizeUrl() { - return '$baseUrl/authorize${buildQueryString({'client_id': clientId, 'scope': scopes.join(','), 'redirect_uri': redirectUri, 'state': state})}'; + String createAuthorizeUrl(String codeChallenge) { + return '$baseUrl/authorize${buildQueryString({'client_id': clientId, 'scope': scopes.join(','), 'redirect_uri': redirectUri, 'state': state, 'code_challenge': codeChallenge, 'code_challenge_method': 'S256'})}'; } Future exchange( From 1bb8dd6203596576d814ffdd946a354031a8a2ce Mon Sep 17 00:00:00 2001 From: IamPekka058 <59747867+IamPekka058@users.noreply.github.com> Date: Sat, 2 Aug 2025 00:42:31 +0200 Subject: [PATCH 3/3] :sparkles: Add PKCE utility export and remove unused ExchangeResponse class --- lib/src/common.dart | 1 + lib/src/common/util/oauth2_pkce.dart | 9 --------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/lib/src/common.dart b/lib/src/common.dart index a0726858..018fa2a0 100644 --- a/lib/src/common.dart +++ b/lib/src/common.dart @@ -9,6 +9,7 @@ export 'package:github_flutter/src/common/util/device_flow.dart'; export 'package:github_flutter/src/common/util/errors.dart'; export 'package:github_flutter/src/common/util/json.dart'; export 'package:github_flutter/src/common/util/oauth2.dart'; +export 'package:github_flutter/src/common/util/oauth2_pkce.dart'; export 'package:github_flutter/src/common/util/pagination.dart'; export 'package:github_flutter/src/common/util/service.dart'; export 'package:github_flutter/src/common/util/utils.dart'; diff --git a/lib/src/common/util/oauth2_pkce.dart b/lib/src/common/util/oauth2_pkce.dart index dd82d924..f6e96b8d 100644 --- a/lib/src/common/util/oauth2_pkce.dart +++ b/lib/src/common/util/oauth2_pkce.dart @@ -87,12 +87,3 @@ class OAuth2PKCE { }); } } - -/// Represents a response for exchanging a code for a token. -class ExchangeResponse { - final String? token; - final List scopes; - final String? tokenType; - - ExchangeResponse(this.token, this.tokenType, this.scopes); -}