Need to work out tokens

This commit is contained in:
Tobe O 2017-09-28 22:16:44 -04:00
parent 8fe36310e9
commit 4a14b795f4
17 changed files with 537 additions and 43 deletions

74
.gitignore vendored
View file

@ -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

View 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
View 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
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View file

@ -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
View file

@ -0,0 +1,3 @@
export 'src/response.dart';
export 'src/server.dart';
export 'src/token_type.dart';

View file

@ -1 +0,0 @@
export 'src/server.dart';

2
lib/src/auth_code.dart Normal file
View file

@ -0,0 +1,2 @@
library auth_oauth2_server.src.auth_code;

36
lib/src/exception.dart Normal file
View 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);
}

View file

@ -1,3 +0,0 @@
abstract class Grant {
}

12
lib/src/response.dart Normal file
View 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;
}
}

View file

@ -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
View file

@ -0,0 +1,3 @@
import 'package:angel_auth/angel_auth.dart';
import 'server.dart';

3
lib/src/token_type.dart Normal file
View file

@ -0,0 +1,3 @@
abstract class TokenType {
static const String bearer = 'bearer', mac = 'mac';
}

View file

@ -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
View 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
View 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);
}