| // Copyright (c) 2012, 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'; |
| import 'dart:convert'; |
| |
| import 'package:http/http.dart' as http; |
| import 'package:oauth2/oauth2.dart' as oauth2; |
| import 'package:test/test.dart'; |
| |
| import 'utils.dart'; |
| |
| final redirectUrl = Uri.parse('http://example.com/redirect'); |
| |
| void main() { |
| late ExpectClient client; |
| late oauth2.AuthorizationCodeGrant grant; |
| setUp(() { |
| client = ExpectClient(); |
| grant = oauth2.AuthorizationCodeGrant( |
| 'identifier', |
| Uri.parse('https://example.com/authorization'), |
| Uri.parse('https://example.com/token'), |
| secret: 'secret', |
| httpClient: client); |
| }); |
| |
| group('.getAuthorizationUrl', () { |
| test('builds the correct URL', () { |
| expect( |
| grant.getAuthorizationUrl(redirectUrl).toString(), |
| allOf([ |
| startsWith('https://example.com/authorization?response_type=code'), |
| contains('&client_id=identifier'), |
| contains('&redirect_uri=http%3A%2F%2Fexample.com%2Fredirect'), |
| matches(r'&code_challenge=[A-Za-z0-9\+\/\-\_]{43}'), |
| contains('&code_challenge_method=S256') |
| ])); |
| }); |
| |
| test('builds the correct URL with scopes', () { |
| var authorizationUrl = grant |
| .getAuthorizationUrl(redirectUrl, scopes: ['scope', 'other/scope']); |
| expect( |
| authorizationUrl.toString(), |
| allOf([ |
| startsWith('https://example.com/authorization?response_type=code'), |
| contains('&client_id=identifier'), |
| contains('&redirect_uri=http%3A%2F%2Fexample.com%2Fredirect'), |
| matches(r'&code_challenge=[A-Za-z0-9\+\/\-\_]{43}'), |
| contains('&code_challenge_method=S256'), |
| contains('&scope=scope+other%2Fscope') |
| ])); |
| }); |
| |
| test('builds the correct URL with passed in code verifier', () { |
| const codeVerifier = |
| 'it1shei7LooGoh3looxaa4sieveijeib2zecauz2oo8aebae5aehee0ahPirewoh' |
| '5Bo6Maexooqui3uL2si6ahweiv7shauc1shahxooveoB3aeyahsaiye0Egh3raix'; |
| const expectedCodeChallenge = |
| 'EjfFMv8TFPd3GuNxAn5COhlWBGpfZLimHett7ypJfJ0'; |
| var grant = oauth2.AuthorizationCodeGrant( |
| 'identifier', |
| Uri.parse('https://example.com/authorization'), |
| Uri.parse('https://example.com/token'), |
| secret: 'secret', |
| httpClient: client, |
| codeVerifier: codeVerifier); |
| expect( |
| grant.getAuthorizationUrl(redirectUrl).toString(), |
| allOf([ |
| startsWith('https://example.com/authorization?response_type=code'), |
| contains('&client_id=identifier'), |
| contains('&redirect_uri=http%3A%2F%2Fexample.com%2Fredirect'), |
| contains('&code_challenge=$expectedCodeChallenge'), |
| contains('&code_challenge_method=S256') |
| ])); |
| }); |
| |
| test('separates scopes with the correct delimiter', () { |
| var grant = oauth2.AuthorizationCodeGrant( |
| 'identifier', |
| Uri.parse('https://example.com/authorization'), |
| Uri.parse('https://example.com/token'), |
| secret: 'secret', |
| httpClient: client, |
| delimiter: '_'); |
| var authorizationUrl = grant |
| .getAuthorizationUrl(redirectUrl, scopes: ['scope', 'other/scope']); |
| expect( |
| authorizationUrl.toString(), |
| allOf([ |
| startsWith('https://example.com/authorization?response_type=code'), |
| contains('&client_id=identifier'), |
| contains('&redirect_uri=http%3A%2F%2Fexample.com%2Fredirect'), |
| matches(r'&code_challenge=[A-Za-z0-9\+\/\-\_]{43}'), |
| contains('&code_challenge_method=S256'), |
| contains('&scope=scope_other%2Fscope') |
| ])); |
| }); |
| |
| test('builds the correct URL with state', () { |
| var authorizationUrl = |
| grant.getAuthorizationUrl(redirectUrl, state: 'state'); |
| expect( |
| authorizationUrl.toString(), |
| allOf([ |
| startsWith('https://example.com/authorization?response_type=code'), |
| contains('&client_id=identifier'), |
| contains('&redirect_uri=http%3A%2F%2Fexample.com%2Fredirect'), |
| matches(r'&code_challenge=[A-Za-z0-9\+\/\-\_]{43}'), |
| contains('&code_challenge_method=S256'), |
| contains('&state=state') |
| ])); |
| }); |
| |
| test('merges with existing query parameters', () { |
| grant = oauth2.AuthorizationCodeGrant( |
| 'identifier', |
| Uri.parse('https://example.com/authorization?query=value'), |
| Uri.parse('https://example.com/token'), |
| secret: 'secret', |
| httpClient: client); |
| |
| var authorizationUrl = grant.getAuthorizationUrl(redirectUrl); |
| expect( |
| authorizationUrl.toString(), |
| allOf([ |
| startsWith('https://example.com/authorization?query=value'), |
| contains('&response_type=code'), |
| contains('&client_id=identifier'), |
| contains('&redirect_uri=http%3A%2F%2Fexample.com%2Fredirect'), |
| matches(r'&code_challenge=[A-Za-z0-9\+\/\-\_]{43}'), |
| contains('&code_challenge_method=S256'), |
| ])); |
| }); |
| |
| test("can't be called twice", () { |
| grant.getAuthorizationUrl(redirectUrl); |
| expect(() => grant.getAuthorizationUrl(redirectUrl), throwsStateError); |
| }); |
| }); |
| |
| group('.handleAuthorizationResponse', () { |
| test("can't be called before .getAuthorizationUrl", () { |
| expect(grant.handleAuthorizationResponse({}), throwsStateError); |
| }); |
| |
| test("can't be called twice", () { |
| grant.getAuthorizationUrl(redirectUrl); |
| expect(grant.handleAuthorizationResponse({'code': 'auth code'}), |
| throwsFormatException); |
| expect(grant.handleAuthorizationResponse({'code': 'auth code'}), |
| throwsStateError); |
| }); |
| |
| test('must have a state parameter if the authorization URL did', () { |
| grant.getAuthorizationUrl(redirectUrl, state: 'state'); |
| expect(grant.handleAuthorizationResponse({'code': 'auth code'}), |
| throwsFormatException); |
| }); |
| |
| test('must have the same state parameter the authorization URL did', () { |
| grant.getAuthorizationUrl(redirectUrl, state: 'state'); |
| expect( |
| grant.handleAuthorizationResponse( |
| {'code': 'auth code', 'state': 'other state'}), |
| throwsFormatException); |
| }); |
| |
| test('must have a code parameter', () { |
| grant.getAuthorizationUrl(redirectUrl); |
| expect(grant.handleAuthorizationResponse({}), throwsFormatException); |
| }); |
| |
| test('with an error parameter throws an AuthorizationException', () { |
| grant.getAuthorizationUrl(redirectUrl); |
| expect(grant.handleAuthorizationResponse({'error': 'invalid_request'}), |
| throwsA(isA<oauth2.AuthorizationException>())); |
| }); |
| |
| test('sends an authorization code request', () { |
| grant.getAuthorizationUrl(redirectUrl); |
| client.expectRequest((request) { |
| expect(request.method, equals('POST')); |
| expect(request.url.toString(), equals(grant.tokenEndpoint.toString())); |
| expect( |
| request.bodyFields, |
| allOf([ |
| containsPair('grant_type', 'authorization_code'), |
| containsPair('code', 'auth code'), |
| containsPair('redirect_uri', redirectUrl.toString()), |
| containsPair( |
| 'code_verifier', matches(r'[A-Za-z0-9\-\.\_\~]{128}')) |
| ])); |
| expect(request.headers, |
| containsPair('Authorization', 'Basic aWRlbnRpZmllcjpzZWNyZXQ=')); |
| |
| return Future.value(http.Response( |
| jsonEncode({ |
| 'access_token': 'access token', |
| 'token_type': 'bearer', |
| }), |
| 200, |
| headers: {'content-type': 'application/json'})); |
| }); |
| |
| expect( |
| grant.handleAuthorizationResponse({'code': 'auth code'}).then( |
| (client) => client.credentials.accessToken), |
| completion(equals('access token'))); |
| }); |
| }); |
| |
| group('.handleAuthorizationCode', () { |
| test("can't be called before .getAuthorizationUrl", () { |
| expect(grant.handleAuthorizationCode('auth code'), throwsStateError); |
| }); |
| |
| test("can't be called twice", () { |
| grant.getAuthorizationUrl(redirectUrl); |
| expect(grant.handleAuthorizationCode('auth code'), throwsFormatException); |
| expect(grant.handleAuthorizationCode('auth code'), throwsStateError); |
| }); |
| |
| test('sends an authorization code request', () async { |
| grant.getAuthorizationUrl(redirectUrl); |
| client.expectRequest((request) { |
| expect(request.method, equals('POST')); |
| expect(request.url.toString(), equals(grant.tokenEndpoint.toString())); |
| expect( |
| request.bodyFields, |
| allOf([ |
| containsPair('grant_type', 'authorization_code'), |
| containsPair('code', 'auth code'), |
| containsPair('redirect_uri', redirectUrl.toString()), |
| containsPair( |
| 'code_verifier', matches(r'[A-Za-z0-9\-\.\_\~]{128}')) |
| ])); |
| expect(request.headers, |
| containsPair('Authorization', 'Basic aWRlbnRpZmllcjpzZWNyZXQ=')); |
| |
| return Future.value(http.Response( |
| jsonEncode({ |
| 'access_token': 'access token', |
| 'token_type': 'bearer', |
| }), |
| 200, |
| headers: {'content-type': 'application/json'})); |
| }); |
| |
| expect( |
| await grant.handleAuthorizationCode('auth code'), |
| isA<oauth2.Client>().having((c) => c.credentials.accessToken, |
| 'credentials.accessToken', 'access token')); |
| }); |
| }); |
| |
| group('with basicAuth: false', () { |
| setUp(() { |
| client = ExpectClient(); |
| grant = oauth2.AuthorizationCodeGrant( |
| 'identifier', |
| Uri.parse('https://example.com/authorization'), |
| Uri.parse('https://example.com/token'), |
| secret: 'secret', |
| basicAuth: false, |
| httpClient: client); |
| }); |
| |
| test('.handleAuthorizationResponse sends an authorization code request', |
| () { |
| grant.getAuthorizationUrl(redirectUrl); |
| client.expectRequest((request) { |
| expect(request.method, equals('POST')); |
| expect(request.url.toString(), equals(grant.tokenEndpoint.toString())); |
| expect( |
| request.bodyFields, |
| allOf([ |
| containsPair('grant_type', 'authorization_code'), |
| containsPair('code', 'auth code'), |
| containsPair('redirect_uri', redirectUrl.toString()), |
| containsPair( |
| 'code_verifier', matches(r'[A-Za-z0-9\-\.\_\~]{128}')), |
| containsPair('client_id', 'identifier'), |
| containsPair('client_secret', 'secret') |
| ])); |
| |
| return Future.value(http.Response( |
| jsonEncode({ |
| 'access_token': 'access token', |
| 'token_type': 'bearer', |
| }), |
| 200, |
| headers: {'content-type': 'application/json'})); |
| }); |
| |
| expect( |
| grant.handleAuthorizationResponse({'code': 'auth code'}).then( |
| (client) => client.credentials.accessToken), |
| completion(equals('access token'))); |
| }); |
| |
| test('.handleAuthorizationCode sends an authorization code request', |
| () async { |
| grant.getAuthorizationUrl(redirectUrl); |
| client.expectRequest((request) { |
| expect(request.method, equals('POST')); |
| expect(request.url.toString(), equals(grant.tokenEndpoint.toString())); |
| expect( |
| request.bodyFields, |
| allOf([ |
| containsPair('grant_type', 'authorization_code'), |
| containsPair('code', 'auth code'), |
| containsPair('redirect_uri', redirectUrl.toString()), |
| containsPair( |
| 'code_verifier', matches(r'[A-Za-z0-9\-\.\_\~]{128}')), |
| containsPair('client_id', 'identifier'), |
| containsPair('client_secret', 'secret') |
| ])); |
| |
| return Future.value(http.Response( |
| jsonEncode({ |
| 'access_token': 'access token', |
| 'token_type': 'bearer', |
| }), |
| 200, |
| headers: {'content-type': 'application/json'})); |
| }); |
| |
| expect( |
| await grant.handleAuthorizationCode('auth code'), |
| isA<oauth2.Client>().having((c) => c.credentials.accessToken, |
| 'credentials.accessToken', 'access token')); |
| }); |
| }); |
| |
| group('onCredentialsRefreshed', () { |
| test('is correctly propagated', () async { |
| var isCallbackInvoked = false; |
| var grant = oauth2.AuthorizationCodeGrant( |
| 'identifier', |
| Uri.parse('https://example.com/authorization'), |
| Uri.parse('https://example.com/token'), |
| secret: 'secret', |
| basicAuth: false, |
| httpClient: client, onCredentialsRefreshed: (credentials) { |
| isCallbackInvoked = true; |
| }); |
| |
| grant.getAuthorizationUrl(redirectUrl); |
| client.expectRequest( |
| (request) => Future.value( |
| http.Response( |
| jsonEncode({ |
| 'access_token': 'access token', |
| 'token_type': 'bearer', |
| 'expires_in': -3600, |
| 'refresh_token': 'refresh token', |
| }), |
| 200, |
| headers: {'content-type': 'application/json'}, |
| ), |
| ), |
| ); |
| |
| var oauth2Client = await grant.handleAuthorizationCode('auth code'); |
| |
| client.expectRequest( |
| (request) => Future.value( |
| http.Response( |
| jsonEncode({ |
| 'access_token': 'new access token', |
| 'token_type': 'bearer', |
| }), |
| 200, |
| headers: {'content-type': 'application/json'}, |
| ), |
| ), |
| ); |
| |
| client.expectRequest( |
| (request) => Future.value(http.Response('good job', 200))); |
| |
| await oauth2Client.read(Uri.parse('http://example.com/resource')); |
| |
| expect(isCallbackInvoked, equals(true)); |
| }); |
| }); |
| } |