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
|
# See https://www.dartlang.org/tools/private-files.html
|
||||||
|
|
||||||
# Files and directories created by pub
|
# Files and directories created by pub
|
||||||
.buildlog
|
|
||||||
.packages
|
.packages
|
||||||
.project
|
|
||||||
.pub/
|
.pub/
|
||||||
.scripts-bin/
|
|
||||||
build/
|
build/
|
||||||
**/packages/
|
# If you're building an application, you may want to check-in your pubspec.lock
|
||||||
|
pubspec.lock
|
||||||
# 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
|
|
||||||
|
|
||||||
# Directory created by dartdoc
|
# Directory created by dartdoc
|
||||||
|
# If you don't generate documentation locally you can remove this line.
|
||||||
doc/api/
|
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
|
# auth_oauth2
|
||||||
angel_auth strategy for in-house OAuth2 login.
|
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 'package:angel_framework/angel_framework.dart';
|
||||||
|
import 'exception.dart';
|
||||||
|
import 'response.dart';
|
||||||
|
import 'token_type.dart';
|
||||||
|
|
||||||
abstract class OAuth2Server {
|
String _getParam(RequestContext req, String name, {bool body: false}) {
|
||||||
final AngelAuth auth;
|
var map = body == true ? req.body : req.query;
|
||||||
|
var value = map.containsKey(name) ? map[name]?.toString() : null;
|
||||||
|
|
||||||
RequestMiddleware verifyAuthToken() {
|
if (value?.isNotEmpty != true)
|
||||||
return (req, res) async {
|
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>"
|
name: angel_oauth2
|
||||||
description: "angel_auth strategy for in-house OAuth2 login."
|
author: Tobe O <thosakwe@gmail.com>
|
||||||
homepage: "https://github.com/thosakwe/oauth2_server.git"
|
description: angel_auth strategy for in-house OAuth2 login.
|
||||||
name: "angel_auth_oauth2_server"
|
homepage: https://github.com/thosakwe/oauth2_server.git
|
||||||
version: "0.0.0"
|
version: 1.0.0-alpha
|
||||||
|
environment:
|
||||||
|
sdk: ">=1.19.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
angel_auth: "^1.0.0"
|
angel_auth: ^1.1.0-alpha
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
angel_test: "^1.0.0-dev"
|
angel_test: ^1.1.0-alpha
|
||||||
http: "^0.11.3+9"
|
oauth2: ^1.0.0
|
||||||
oauth2: "^1.0.2"
|
test: ^0.12.0
|
||||||
test: "^0.12.18+1"
|
|
||||||
|
|
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