diff --git a/github_device_auth/README.md b/github_device_auth/README.md index c38b956..c788bcb 100644 --- a/github_device_auth/README.md +++ b/github_device_auth/README.md @@ -34,7 +34,7 @@ dependencies: github_device_auth: git: url: https://github.com/kuhyx/utils - ref: github_device_auth-v0.1.0 + ref: github_device_auth-v0.2.0 path: github_device_auth ``` @@ -48,6 +48,15 @@ final token = await showDialog( auth.close(); ``` +## Configurable endpoints (web builds) + +`deviceCodeUrl` / `tokenUrl` default to GitHub's own URLs, which is right for +mobile and desktop. A **web** build must point both at a local proxy instead: +GitHub's device-flow endpoints send no CORS headers, so a page cannot call +them at all. diet-guard's desktop web build does exactly that, which is why +this knob exists — it was in diet-guard's local copy and would have been lost +had the package shipped only todo's simpler version. + ## One behaviour change from the extracted copies The dialog now catches `on Object`, not `on Exception`. @@ -63,7 +72,7 @@ pins it with a mock that throws an `Error` rather than an `Exception`. ```bash flutter pub get -flutter test # 20 tests +flutter test # 23 tests flutter test --coverage # 100% of lines flutter analyze # very_good_analysis, clean ``` diff --git a/github_device_auth/lib/github_device_auth.dart b/github_device_auth/lib/github_device_auth.dart index 102bf83..ab21520 100644 --- a/github_device_auth/lib/github_device_auth.dart +++ b/github_device_auth/lib/github_device_auth.dart @@ -18,5 +18,10 @@ library; export 'src/device_auth.dart' - show DeviceAuthException, DeviceCodeResponse, GitHubDeviceAuth; + show + DeviceAuthException, + DeviceCodeResponse, + GitHubDeviceAuth, + githubDeviceCodeUrl, + githubTokenUrl; export 'src/device_code_dialog.dart' show DeviceCodeDialog; diff --git a/github_device_auth/lib/src/device_auth.dart b/github_device_auth/lib/src/device_auth.dart index f22ca5c..5dc6ecb 100644 --- a/github_device_auth/lib/src/device_auth.dart +++ b/github_device_auth/lib/src/device_auth.dart @@ -3,6 +3,12 @@ import 'dart:convert'; import 'package:http/http.dart' as http; +/// GitHub's device-code endpoint. The default for [GitHubDeviceAuth]. +const githubDeviceCodeUrl = 'https://github.com/login/device/code'; + +/// GitHub's device-flow token endpoint. The default for [GitHubDeviceAuth]. +const githubTokenUrl = 'https://github.com/login/oauth/access_token'; + /// First-stage response of the GitHub OAuth Device Flow: the code the user /// types on github.com and the URL to type it into. class DeviceCodeResponse { @@ -72,6 +78,8 @@ class GitHubDeviceAuth { GitHubDeviceAuth({ required this.clientId, this.scope = 'repo', + this.deviceCodeUrl = githubDeviceCodeUrl, + this.tokenUrl = githubTokenUrl, http.Client? httpClient, Future Function(Duration)? delay, }) : _http = httpClient ?? http.Client(), @@ -84,17 +92,27 @@ class GitHubDeviceAuth { /// OAuth scope requested. `repo` is required for private-repo contents. final String scope; + /// Endpoint asked for a device + user code. + /// + /// Defaults to GitHub's own URL, which is right for mobile and desktop. + /// A **web** build must point this (and [tokenUrl]) at a local proxy + /// instead: GitHub's device-flow endpoints send no CORS headers, so a page + /// cannot call them at all. diet-guard's desktop web build does exactly + /// that — see its `desktop/github_proxy.dart`. + final String deviceCodeUrl; + + /// Endpoint polled for the access token. See [deviceCodeUrl]. + final String tokenUrl; + final http.Client _http; final Future Function(Duration) _delay; - static const _deviceCodeUrl = 'https://github.com/login/device/code'; - static const _tokenUrl = 'https://github.com/login/oauth/access_token'; static const _grantType = 'urn:ietf:params:oauth:grant-type:device_code'; /// Step 1: ask GitHub for a device + user code. Future requestDeviceCode() async { final res = await _http.post( - Uri.parse(_deviceCodeUrl), + Uri.parse(deviceCodeUrl), headers: const {'Accept': 'application/json'}, body: {'client_id': clientId, 'scope': scope}, ); @@ -118,7 +136,7 @@ class GitHubDeviceAuth { while (DateTime.now().isBefore(deadline)) { await _delay(Duration(seconds: intervalSeconds)); final res = await _http.post( - Uri.parse(_tokenUrl), + Uri.parse(tokenUrl), headers: const {'Accept': 'application/json'}, body: { 'client_id': clientId, diff --git a/github_device_auth/pubspec.yaml b/github_device_auth/pubspec.yaml index abba60c..564bf50 100644 --- a/github_device_auth/pubspec.yaml +++ b/github_device_auth/pubspec.yaml @@ -4,7 +4,7 @@ description: >- modal that drives it. Extracted from four apps that each carried a copy, two of them byte-identical. publish_to: 'none' -version: 0.1.0 +version: 0.2.0 environment: sdk: ^3.12.2 diff --git a/github_device_auth/test/device_auth_test.dart b/github_device_auth/test/device_auth_test.dart index a5ed488..dafd353 100644 --- a/github_device_auth/test/device_auth_test.dart +++ b/github_device_auth/test/device_auth_test.dart @@ -80,6 +80,39 @@ void main() { expect(device.userCode, 'UC'); }); + test('defaults to GitHub s own endpoints', () { + final auth = GitHubDeviceAuth(clientId: 'cid')..close(); + expect(auth.deviceCodeUrl, githubDeviceCodeUrl); + expect(auth.tokenUrl, githubTokenUrl); + }); + + test('honours an overridden endpoint', () async { + // diet-guard's desktop web build routes both endpoints through a local + // proxy, because GitHub's device-flow endpoints send no CORS headers + // and a page cannot call them at all. + late Uri seen; + final auth = GitHubDeviceAuth( + clientId: 'cid', + deviceCodeUrl: 'http://127.0.0.1:9/gh/auth/device/start', + httpClient: MockClient((req) async { + seen = req.url; + return http.Response( + jsonEncode({ + 'device_code': 'dc', + 'user_code': 'UC', + 'verification_uri': 'u', + }), + 200, + ); + }), + delay: noDelay, + ); + + await auth.requestDeviceCode(); + + expect(seen.toString(), 'http://127.0.0.1:9/gh/auth/device/start'); + }); + test('throws with the status code on a non-200', () async { final auth = authWith(MockClient((_) async => http.Response('bad', 503))); await expectLater( @@ -228,4 +261,29 @@ void main() { final auth = GitHubDeviceAuth(clientId: 'cid')..close(); expect(auth.scope, 'repo'); }); + + test('polls the overridden token endpoint', () async { + late Uri seen; + final auth = GitHubDeviceAuth( + clientId: 'cid', + tokenUrl: 'http://127.0.0.1:9/gh/auth/device/poll', + httpClient: MockClient((req) async { + seen = req.url; + return http.Response(jsonEncode({'access_token': 'tok'}), 200); + }), + delay: noDelay, + ); + + await auth.pollForToken( + const DeviceCodeResponse( + deviceCode: 'dc', + userCode: 'UC', + verificationUri: 'u', + interval: 1, + expiresIn: 60, + ), + ); + + expect(seen.toString(), 'http://127.0.0.1:9/gh/auth/device/poll'); + }); }