Need to work out tokens
This commit is contained in:
parent
8fe36310e9
commit
4a14b795f4
17 changed files with 537 additions and 43 deletions
74
.gitignore
vendored
74
.gitignore
vendored
|
@ -1,28 +1,64 @@
|
|||
# Created by .ignore support plugin (hsz.mobi)
|
||||
### JetBrains template
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff:
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/dictionaries
|
||||
|
||||
# Sensitive or high-churn files:
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.xml
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
|
||||
# Gradle:
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
|
||||
# CMake
|
||||
cmake-build-debug/
|
||||
|
||||
# Mongo Explorer plugin:
|
||||
.idea/**/mongoSettings.xml
|
||||
|
||||
## File-based project format:
|
||||
*.iws
|
||||
|
||||
## Plugin-specific files:
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Cursive Clojure plugin
|
||||
.idea/replstate.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
### Dart template
|
||||
# See https://www.dartlang.org/tools/private-files.html
|
||||
|
||||
# Files and directories created by pub
|
||||
.buildlog
|
||||
.packages
|
||||
.project
|
||||
.pub/
|
||||
.scripts-bin/
|
||||
build/
|
||||
**/packages/
|
||||
|
||||
# Files created by dart2js
|
||||
# (Most Dart developers will use pub build to compile Dart, use/modify these
|
||||
# rules if you intend to use dart2js directly
|
||||
# Convention is to use extension '.dart.js' for Dart compiled to Javascript to
|
||||
# differentiate from explicit Javascript files)
|
||||
*.dart.js
|
||||
*.part.js
|
||||
*.js.deps
|
||||
*.js.map
|
||||
*.info.json
|
||||
# If you're building an application, you may want to check-in your pubspec.lock
|
||||
pubspec.lock
|
||||
|
||||
# Directory created by dartdoc
|
||||
# If you don't generate documentation locally you can remove this line.
|
||||
doc/api/
|
||||
|
||||
# Don't commit pubspec lock file
|
||||
# (Library packages only! Remove pattern if developing an application package)
|
||||
pubspec.lock
|
||||
|
|
14
.idea/auth_oauth2_server.iml
Normal file
14
.idea/auth_oauth2_server.iml
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="JAVA_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.pub" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
<orderEntry type="library" name="Dart SDK" level="project" />
|
||||
<orderEntry type="library" name="Dart Packages" level="project" />
|
||||
</component>
|
||||
</module>
|
8
.idea/modules.xml
Normal file
8
.idea/modules.xml
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/auth_oauth2_server.iml" filepath="$PROJECT_DIR$/.idea/auth_oauth2_server.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
6
.idea/vcs.xml
Normal file
6
.idea/vcs.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
92
README.md
92
README.md
|
@ -1,2 +1,90 @@
|
|||
# auth_oauth2_server
|
||||
angel_auth strategy for in-house OAuth2 login.
|
||||
# auth_oauth2
|
||||
A class containing handlers that can be used within
|
||||
[Angel](https://angel-dart.github.io/) to build a spec-compliant
|
||||
OAuth 2.0 server.
|
||||
|
||||
# Installation
|
||||
In your `pubspec.yaml`:
|
||||
|
||||
```yaml
|
||||
dependencies:
|
||||
angel_oauth2: ^1.0.0
|
||||
```
|
||||
|
||||
# Usage
|
||||
Your server needs to have definitions of at least two types:
|
||||
* One model that represents a third-party application (client) trying to access a user's profile.
|
||||
* One that represents a user logged into the application.
|
||||
|
||||
Define a server class as such:
|
||||
|
||||
```dart
|
||||
import 'package:angel_oauth2/angel_oauth2.dart' as oauth2;
|
||||
|
||||
class MyServer extends oauth2.Server<Client, User> {}
|
||||
```
|
||||
|
||||
Then, implement the `findClient` and `verifyClient` to ensure that the
|
||||
server class can not only identify a client application via a `client_id`,
|
||||
but that it can also verify its identity via a `client_secret`.
|
||||
|
||||
```dart
|
||||
class _Server extends Server<PseudoApplication, Map> {
|
||||
final Uuid _uuid = new Uuid();
|
||||
|
||||
@override
|
||||
FutureOr<PseudoApplication> findClient(String clientId) {
|
||||
return clientId == pseudoApplication.id ? pseudoApplication : null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> verifyClient(
|
||||
PseudoApplication client, String clientSecret) async {
|
||||
return client.secret == clientSecret;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Next, write some logic to be executed whenever a user visits the
|
||||
authorization endpoint. In most cases, you will want to show a dialog:
|
||||
|
||||
```dart
|
||||
@override
|
||||
Future authorize(
|
||||
PseudoApplication client,
|
||||
String redirectUri,
|
||||
Iterable<String> scopes,
|
||||
String state,
|
||||
RequestContext req,
|
||||
ResponseContext res) async {
|
||||
res.render('dialog');
|
||||
}
|
||||
```
|
||||
|
||||
Now, write logic that exchanges an authorization code for an access token,
|
||||
and optionally, a refresh token.
|
||||
|
||||
```dart
|
||||
@override
|
||||
Future<AuthorizationCodeResponse> exchangeAuthCodeForAccessToken(
|
||||
String authCode,
|
||||
String redirectUri,
|
||||
RequestContext req,
|
||||
ResponseContext res) async {
|
||||
return new AuthorizationCodeResponse('foo', refreshToken: 'bar');
|
||||
}
|
||||
```
|
||||
|
||||
Now, set up some routes to point the server.
|
||||
|
||||
```dart
|
||||
void pseudoCode() {
|
||||
app.group('/oauth2', (router) {
|
||||
router
|
||||
..get('/authorize', server.authorizationEndpoint)
|
||||
..post('/token', server.tokenEndpoint);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
Naturally,
|
3
lib/angel_oauth2.dart
Normal file
3
lib/angel_oauth2.dart
Normal file
|
@ -0,0 +1,3 @@
|
|||
export 'src/response.dart';
|
||||
export 'src/server.dart';
|
||||
export 'src/token_type.dart';
|
|
@ -1 +0,0 @@
|
|||
export 'src/server.dart';
|
2
lib/src/auth_code.dart
Normal file
2
lib/src/auth_code.dart
Normal file
|
@ -0,0 +1,2 @@
|
|||
library auth_oauth2_server.src.auth_code;
|
||||
|
36
lib/src/exception.dart
Normal file
36
lib/src/exception.dart
Normal file
|
@ -0,0 +1,36 @@
|
|||
import 'package:angel_http_exception/angel_http_exception.dart';
|
||||
|
||||
class AuthorizationException extends AngelHttpException {
|
||||
final ErrorResponse errorResponse;
|
||||
|
||||
AuthorizationException(this.errorResponse,
|
||||
{StackTrace stackTrace, int statusCode})
|
||||
: super(errorResponse,
|
||||
stackTrace: stackTrace, message: '', statusCode: statusCode ?? 401);
|
||||
}
|
||||
|
||||
class ErrorResponse {
|
||||
final String code, description;
|
||||
|
||||
// Taken from https://www.docusign.com/p/RESTAPIGuide/Content/OAuth2/OAuth2%20Response%20Codes.htm
|
||||
// TODO: Use original error messages
|
||||
static const ErrorResponse invalidRequest = const ErrorResponse(
|
||||
'invalid_request',
|
||||
'The request was malformed, or contains unsupported parameters.'),
|
||||
invalidClient = const ErrorResponse(
|
||||
'invalid_client', 'The client authentication failed.'),
|
||||
invalidGrant = const ErrorResponse(
|
||||
'invalid_grant', 'The provided authorization is invalid.'),
|
||||
unauthorizedClient = const ErrorResponse('unauthorized_client',
|
||||
'The client application is not allowed to use this grant_type.'),
|
||||
unauthorizedGrantType = const ErrorResponse('unsupported_grant_type',
|
||||
'A grant_type other than “password” was used in the request.'),
|
||||
invalidScope = const ErrorResponse(
|
||||
'invalid_scope', 'One or more of the scopes you provided was invalid.'),
|
||||
unsupportedTokenType = const ErrorResponse('unsupported_token_type',
|
||||
'The client tried to revoke an access token on a server not supporting this feature.'),
|
||||
invalidToken = const ErrorResponse(
|
||||
'invalid_token', 'The presented token is invalid.');
|
||||
|
||||
const ErrorResponse(this.code, this.description);
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
abstract class Grant {
|
||||
|
||||
}
|
12
lib/src/response.dart
Normal file
12
lib/src/response.dart
Normal file
|
@ -0,0 +1,12 @@
|
|||
class AuthorizationCodeResponse {
|
||||
final String accessToken;
|
||||
final String refreshToken;
|
||||
|
||||
const AuthorizationCodeResponse(this.accessToken, {this.refreshToken});
|
||||
|
||||
Map<String, String> toJson() {
|
||||
var map = <String, String> {'access_token': accessToken};
|
||||
if (refreshToken?.isNotEmpty == true) map['refresh_token'] = refreshToken;
|
||||
return map;
|
||||
}
|
||||
}
|
|
@ -1,12 +1,119 @@
|
|||
import 'package:angel_auth/angel_auth.dart';
|
||||
import 'dart:async';
|
||||
import 'package:angel_framework/angel_framework.dart';
|
||||
import 'exception.dart';
|
||||
import 'response.dart';
|
||||
import 'token_type.dart';
|
||||
|
||||
abstract class OAuth2Server {
|
||||
final AngelAuth auth;
|
||||
String _getParam(RequestContext req, String name, {bool body: false}) {
|
||||
var map = body == true ? req.body : req.query;
|
||||
var value = map.containsKey(name) ? map[name]?.toString() : null;
|
||||
|
||||
RequestMiddleware verifyAuthToken() {
|
||||
return (req, res) async {
|
||||
if (value?.isNotEmpty != true)
|
||||
throw new AngelHttpException.badRequest(
|
||||
message: "Missing required parameter '$name'.");
|
||||
|
||||
};
|
||||
return value;
|
||||
}
|
||||
|
||||
Iterable<String> _getScopes(RequestContext req, {bool body: false}) {
|
||||
var map = body == true ? req.body : req.query;
|
||||
return map['scope']?.toString()?.split(' ') ?? [];
|
||||
}
|
||||
|
||||
abstract class Server<Client, User> {
|
||||
const Server();
|
||||
|
||||
/// Finds the [Client] application associated with the given [clientId].
|
||||
FutureOr<Client> findClient(String clientId);
|
||||
|
||||
Future<bool> verifyClient(Client client, String clientSecret);
|
||||
|
||||
Future<String> authCodeGrant(Client client, String redirectUri, User user,
|
||||
Iterable<String> scopes, String state);
|
||||
|
||||
authorize(Client client, String redirectUri, Iterable<String> scopes,
|
||||
String state, RequestContext req, ResponseContext res);
|
||||
|
||||
Future<AuthorizationCodeResponse> exchangeAuthCodeForAccessToken(
|
||||
String authCode,
|
||||
String redirectUri,
|
||||
RequestContext req,
|
||||
ResponseContext res);
|
||||
|
||||
Future authorizationEndpoint(RequestContext req, ResponseContext res) async {
|
||||
var responseType = _getParam(req, 'response_type');
|
||||
|
||||
if (responseType != 'code')
|
||||
throw new AngelHttpException.badRequest(
|
||||
message: "Invalid response_type, expected 'code'.");
|
||||
|
||||
// Ensure client ID
|
||||
var clientId = _getParam(req, 'client_id');
|
||||
|
||||
// Find client
|
||||
var client = await findClient(clientId);
|
||||
|
||||
if (client == null)
|
||||
throw new AuthorizationException(ErrorResponse.invalidClient);
|
||||
|
||||
// Grab redirect URI
|
||||
var redirectUri = _getParam(req, 'redirect_uri');
|
||||
|
||||
// Grab scopes
|
||||
var scopes = _getScopes(req);
|
||||
|
||||
var state = req.query['state']?.toString() ?? '';
|
||||
|
||||
return await authorize(client, redirectUri, scopes, state, req, res);
|
||||
}
|
||||
|
||||
Future tokenEndpoint(RequestContext req, ResponseContext res) async {
|
||||
await req.parse();
|
||||
|
||||
var grantType = _getParam(req, 'grant_type', body: true);
|
||||
|
||||
if (grantType != 'authorization_code')
|
||||
throw new AngelHttpException.badRequest(
|
||||
message: "Invalid grant_type; expected 'authorization_code'.");
|
||||
|
||||
var code = _getParam(req, 'code', body: true);
|
||||
var redirectUri = _getParam(req, 'redirect_uri', body: true);
|
||||
|
||||
var response =
|
||||
await exchangeAuthCodeForAccessToken(code, redirectUri, req, res);
|
||||
return {'token_type': TokenType.bearer}..addAll(response.toJson());
|
||||
}
|
||||
|
||||
Future handleFormSubmission(RequestContext req, ResponseContext res) async {
|
||||
await req.parse();
|
||||
|
||||
// Ensure client ID
|
||||
var clientId = _getParam(req, 'client_id', body: true);
|
||||
|
||||
// Find client
|
||||
var client = await findClient(clientId);
|
||||
|
||||
if (client == null)
|
||||
throw new AuthorizationException(ErrorResponse.invalidClient);
|
||||
|
||||
// Verify client secret
|
||||
var clientSecret = _getParam(req, 'client_secret', body: true);
|
||||
|
||||
if (!await verifyClient(client, clientSecret))
|
||||
throw new AuthorizationException(ErrorResponse.invalidClient);
|
||||
|
||||
// Grab redirect URI
|
||||
var redirectUri = _getParam(req, 'redirect_uri', body: true);
|
||||
|
||||
// Grab scopes
|
||||
var scopes = _getScopes(req, body: true);
|
||||
|
||||
var state = req.query['state']?.toString() ?? '';
|
||||
|
||||
var authCode = await authCodeGrant(
|
||||
client, redirectUri, req.properties['user'], scopes, state);
|
||||
res.headers['content-type'] = 'application/x-www-form-urlencoded';
|
||||
res.write('code=' + Uri.encodeComponent(authCode));
|
||||
if (state.isNotEmpty) res.write('&state=' + Uri.encodeComponent(state));
|
||||
}
|
||||
}
|
3
lib/src/strategy.dart
Normal file
3
lib/src/strategy.dart
Normal file
|
@ -0,0 +1,3 @@
|
|||
import 'package:angel_auth/angel_auth.dart';
|
||||
import 'server.dart';
|
||||
|
3
lib/src/token_type.dart
Normal file
3
lib/src/token_type.dart
Normal file
|
@ -0,0 +1,3 @@
|
|||
abstract class TokenType {
|
||||
static const String bearer = 'bearer', mac = 'mac';
|
||||
}
|
21
pubspec.yaml
21
pubspec.yaml
|
@ -1,12 +1,13 @@
|
|||
author: "Tobe O <thosakwe@gmail.com>"
|
||||
description: "angel_auth strategy for in-house OAuth2 login."
|
||||
homepage: "https://github.com/thosakwe/oauth2_server.git"
|
||||
name: "angel_auth_oauth2_server"
|
||||
version: "0.0.0"
|
||||
name: angel_oauth2
|
||||
author: Tobe O <thosakwe@gmail.com>
|
||||
description: angel_auth strategy for in-house OAuth2 login.
|
||||
homepage: https://github.com/thosakwe/oauth2_server.git
|
||||
version: 1.0.0-alpha
|
||||
environment:
|
||||
sdk: ">=1.19.0"
|
||||
dependencies:
|
||||
angel_auth: "^1.0.0"
|
||||
angel_auth: ^1.1.0-alpha
|
||||
dev_dependencies:
|
||||
angel_test: "^1.0.0-dev"
|
||||
http: "^0.11.3+9"
|
||||
oauth2: "^1.0.2"
|
||||
test: "^0.12.18+1"
|
||||
angel_test: ^1.1.0-alpha
|
||||
oauth2: ^1.0.0
|
||||
test: ^0.12.0
|
||||
|
|
171
test/auth_code_test.dart
Normal file
171
test/auth_code_test.dart
Normal file
|
@ -0,0 +1,171 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:angel_framework/angel_framework.dart';
|
||||
import 'package:angel_oauth2/angel_oauth2.dart';
|
||||
import 'package:angel_test/angel_test.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:oauth2/oauth2.dart' as oauth2;
|
||||
import 'package:test/test.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import 'common.dart';
|
||||
|
||||
main() {
|
||||
Angel app;
|
||||
Uri authorizationEndpoint, tokenEndpoint, redirectUri;
|
||||
TestClient testClient;
|
||||
|
||||
setUp(() async {
|
||||
app = new Angel()..lazyParseBodies = true;
|
||||
app.configuration['properties'] = app.configuration;
|
||||
app.inject('authCodes', <String, String>{});
|
||||
|
||||
var server = new _Server();
|
||||
|
||||
app.group('/oauth2', (router) {
|
||||
router
|
||||
..get('/authorize', server.authorizationEndpoint)
|
||||
..post('/token', server.tokenEndpoint);
|
||||
});
|
||||
|
||||
app.logger = new Logger('angel')
|
||||
..onRecord.listen((rec) {
|
||||
print(rec);
|
||||
if (rec.error != null) print(rec.error);
|
||||
if (rec.stackTrace != null) print(rec.stackTrace);
|
||||
});
|
||||
|
||||
var http = await app.startServer();
|
||||
var url = 'http://${http.address.address}:${http.port}';
|
||||
authorizationEndpoint = Uri.parse('$url/oauth2/authorize');
|
||||
tokenEndpoint = Uri.parse('$url/oauth2/token');
|
||||
redirectUri = Uri.parse('http://foo.bar/baz');
|
||||
|
||||
testClient = await connectTo(app);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await testClient.close();
|
||||
});
|
||||
|
||||
group('auth code', () {
|
||||
oauth2.AuthorizationCodeGrant createGrant() =>
|
||||
new oauth2.AuthorizationCodeGrant(
|
||||
pseudoApplication.id,
|
||||
authorizationEndpoint,
|
||||
tokenEndpoint,
|
||||
secret: pseudoApplication.secret,
|
||||
);
|
||||
|
||||
test('show authorization form', () async {
|
||||
var grant = createGrant();
|
||||
var url = grant.getAuthorizationUrl(redirectUri, state: 'hello');
|
||||
var response = await testClient.client.get(url);
|
||||
print('Body: ${response.body}');
|
||||
expect(
|
||||
response.body,
|
||||
JSON.encode(
|
||||
'Hello ${pseudoApplication.id}:${pseudoApplication.secret}'));
|
||||
});
|
||||
|
||||
test('preserves state', () async {
|
||||
var grant = createGrant();
|
||||
var url = grant.getAuthorizationUrl(redirectUri, state: 'goodbye');
|
||||
var response = await testClient.client.get(url);
|
||||
print('Body: ${response.body}');
|
||||
expect(JSON.decode(response.body)['state'], 'goodbye');
|
||||
});
|
||||
|
||||
test('sends auth code', () async {
|
||||
var grant = createGrant();
|
||||
var url = grant.getAuthorizationUrl(redirectUri);
|
||||
var response = await testClient.client.get(url);
|
||||
print('Body: ${response.body}');
|
||||
expect(
|
||||
JSON.decode(response.body),
|
||||
allOf(
|
||||
isMap,
|
||||
predicate((Map m) => m.containsKey('code'), 'contains "code"'),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('exchange code for token', () async {
|
||||
var grant = createGrant();
|
||||
var url = grant.getAuthorizationUrl(redirectUri);
|
||||
var response = await testClient.client.get(url);
|
||||
print('Body: ${response.body}');
|
||||
|
||||
var authCode = JSON.decode(response.body)['code'];
|
||||
var client = await grant.handleAuthorizationCode(authCode);
|
||||
expect(client.credentials.accessToken, authCode + '_access');
|
||||
});
|
||||
|
||||
test('can send refresh token', () async {
|
||||
var grant = createGrant();
|
||||
var url = grant.getAuthorizationUrl(redirectUri, state: 'can_refresh');
|
||||
var response = await testClient.client.get(url);
|
||||
print('Body: ${response.body}');
|
||||
|
||||
var authCode = JSON.decode(response.body)['code'];
|
||||
var client = await grant.handleAuthorizationCode(authCode);
|
||||
expect(client.credentials.accessToken, authCode + '_access');
|
||||
expect(client.credentials.canRefresh, isTrue);
|
||||
expect(client.credentials.refreshToken, authCode + '_refresh');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class _Server extends Server<PseudoApplication, Map> {
|
||||
final Uuid _uuid = new Uuid();
|
||||
|
||||
@override
|
||||
FutureOr<PseudoApplication> findClient(String clientId) {
|
||||
return clientId == pseudoApplication.id ? pseudoApplication : null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> verifyClient(
|
||||
PseudoApplication client, String clientSecret) async {
|
||||
return client.secret == clientSecret;
|
||||
}
|
||||
|
||||
@override
|
||||
Future authorize(
|
||||
PseudoApplication client,
|
||||
String redirectUri,
|
||||
Iterable<String> scopes,
|
||||
String state,
|
||||
RequestContext req,
|
||||
ResponseContext res) async {
|
||||
if (state == 'hello')
|
||||
return 'Hello ${pseudoApplication.id}:${pseudoApplication.secret}';
|
||||
|
||||
var authCode = _uuid.v4();
|
||||
var authCodes = req.grab<Map<String, String>>('authCodes');
|
||||
authCodes[authCode] = state;
|
||||
|
||||
res.headers['content-type'] = 'application/json';
|
||||
var result = {'code': authCode};
|
||||
if (state?.isNotEmpty == true) result['state'] = state;
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> authCodeGrant(PseudoApplication client, String redirectUri,
|
||||
Map user, Iterable<String> scopes, String state) {
|
||||
throw new UnsupportedError('Nope');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<AuthorizationCodeResponse> exchangeAuthCodeForAccessToken(
|
||||
String authCode,
|
||||
String redirectUri,
|
||||
RequestContext req,
|
||||
ResponseContext res) async {
|
||||
var authCodes = req.grab<Map<String, String>>('authCodes');
|
||||
var state = authCodes[authCode];
|
||||
var refreshToken = state == 'can_refresh' ? '${authCode}_refresh' : null;
|
||||
return new AuthorizationCodeResponse('${authCode}_access',
|
||||
refreshToken: refreshToken);
|
||||
}
|
||||
}
|
8
test/common.dart
Normal file
8
test/common.dart
Normal file
|
@ -0,0 +1,8 @@
|
|||
const PseudoApplication pseudoApplication =
|
||||
const PseudoApplication('foo', 'bar', 'http://foo.bar/baz');
|
||||
|
||||
class PseudoApplication {
|
||||
final String id, secret, redirectUri;
|
||||
|
||||
const PseudoApplication(this.id, this.secret, this.redirectUri);
|
||||
}
|
Loading…
Reference in a new issue