All grants ready
This commit is contained in:
parent
27770c1a69
commit
257d3ce316
14 changed files with 733 additions and 113 deletions
21
README.md
21
README.md
|
@ -1,4 +1,4 @@
|
|||
# auth_oauth2
|
||||
# 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.
|
||||
|
@ -21,7 +21,7 @@ Define a server class as such:
|
|||
```dart
|
||||
import 'package:angel_oauth2/angel_oauth2.dart' as oauth2;
|
||||
|
||||
class MyServer extends oauth2.Server<Client, User> {}
|
||||
class MyServer extends oauth2.AuthorizationServer<Client, User> {}
|
||||
```
|
||||
|
||||
Then, implement the `findClient` and `verifyClient` to ensure that the
|
||||
|
@ -29,7 +29,7 @@ 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> {
|
||||
class _Server extends AuthorizationServer<PseudoApplication, Map> {
|
||||
final Uuid _uuid = new Uuid();
|
||||
|
||||
@override
|
||||
|
@ -50,7 +50,7 @@ authorization endpoint. In most cases, you will want to show a dialog:
|
|||
|
||||
```dart
|
||||
@override
|
||||
Future authorize(
|
||||
Future requestAuthorizationCode(
|
||||
PseudoApplication client,
|
||||
String redirectUri,
|
||||
Iterable<String> scopes,
|
||||
|
@ -87,4 +87,15 @@ void pseudoCode() {
|
|||
}
|
||||
```
|
||||
|
||||
Naturally,
|
||||
The `authorizationEndpoint` and `tokenEndpoint` handle all OAuth2 grant types.
|
||||
|
||||
## Other Grants
|
||||
By default, all OAuth2 grant methods will throw a `405 Method Not Allowed` error.
|
||||
To support any specific grant type, all you need to do is implement the method.
|
||||
The following are available, not including authorization code grant support (mentioned above):
|
||||
* `implicitGrant`
|
||||
* `resourceOwnerPasswordCredentialsGrant`
|
||||
* `clientCredentialsGrant`
|
||||
|
||||
Read the [OAuth2 specification](https://tools.ietf.org/html/rfc6749)
|
||||
for in-depth information on each grant type.
|
|
@ -1,3 +1,4 @@
|
|||
export 'src/exception.dart';
|
||||
export 'src/response.dart';
|
||||
export 'src/server.dart';
|
||||
export 'src/token_type.dart';
|
|
@ -1,2 +0,0 @@
|
|||
library auth_oauth2_server.src.auth_code;
|
||||
|
|
@ -1,36 +1,62 @@
|
|||
import 'package:angel_http_exception/angel_http_exception.dart';
|
||||
|
||||
/// An Angel-friendly wrapper around OAuth2 [ErrorResponse] instances.
|
||||
class AuthorizationException extends AngelHttpException {
|
||||
final ErrorResponse errorResponse;
|
||||
|
||||
AuthorizationException(this.errorResponse,
|
||||
{StackTrace stackTrace, int statusCode})
|
||||
: super(errorResponse,
|
||||
{StackTrace stackTrace, int statusCode, error})
|
||||
: super(error ?? errorResponse,
|
||||
stackTrace: stackTrace, message: '', statusCode: statusCode ?? 401);
|
||||
|
||||
@override
|
||||
Map toJson() {
|
||||
var m = {
|
||||
'error': errorResponse.code,
|
||||
'error_description': errorResponse.description,
|
||||
};
|
||||
|
||||
if (errorResponse.uri != null)
|
||||
m['error_uri'] = errorResponse.uri.toString();
|
||||
|
||||
return m;
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents an OAuth2 authentication error.
|
||||
class ErrorResponse {
|
||||
final String code, description;
|
||||
/// The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed.
|
||||
static const String invalidRequest = 'invalid_request';
|
||||
|
||||
// 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.');
|
||||
/// The client is not authorized to request an authorization code using this method.
|
||||
static const String unauthorizedClient = 'unauthorized_client';
|
||||
|
||||
const ErrorResponse(this.code, this.description);
|
||||
/// The resource owner or authorization server denied the request.
|
||||
static const String accessDenied = 'access_denied';
|
||||
|
||||
/// The authorization server does not support obtaining an authorization code using this method.
|
||||
static const String unsupportedResponseType = 'unsupported_response_type';
|
||||
|
||||
/// The requested scope is invalid, unknown, or malformed.
|
||||
static const String invalidScope = 'invalid_scope';
|
||||
|
||||
/// The authorization server encountered an unexpected condition that prevented it from fulfilling the request.
|
||||
static const String serverError = 'server_error';
|
||||
|
||||
/// The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server.
|
||||
static const String temporarilyUnavailable = 'temporarily_unavailable';
|
||||
|
||||
/// A short string representing the error.
|
||||
final String code;
|
||||
|
||||
/// A relatively detailed description of the source of the error.
|
||||
final String description;
|
||||
|
||||
/// An optional [Uri] directing users to more information about the error.
|
||||
final Uri uri;
|
||||
|
||||
/// The exact value received from the client, if a "state" parameter was present in the client authorization request.
|
||||
final String state;
|
||||
|
||||
const ErrorResponse(this.code, this.description, this.state, {this.uri});
|
||||
}
|
||||
|
|
|
@ -1,12 +1,25 @@
|
|||
class AuthorizationCodeResponse {
|
||||
/// Represents an OAuth2 authorization token.
|
||||
class AuthorizationTokenResponse {
|
||||
/// The string that third parties should use to act on behalf of the user in question.
|
||||
final String accessToken;
|
||||
|
||||
/// An optional key that can be used to refresh the [accessToken] past its expiration.
|
||||
final String refreshToken;
|
||||
|
||||
const AuthorizationCodeResponse(this.accessToken, {this.refreshToken});
|
||||
/// An optional, but recommended integer that signifies the time left until the [accessToken] expires.
|
||||
final int expiresIn;
|
||||
|
||||
Map<String, String> toJson() {
|
||||
var map = <String, String> {'access_token': accessToken};
|
||||
/// Optional, if identical to the scope requested by the client; otherwise, required.
|
||||
final Iterable<String> scope;
|
||||
|
||||
const AuthorizationTokenResponse(this.accessToken,
|
||||
{this.refreshToken, this.expiresIn, this.scope});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
var map = <String, dynamic>{'access_token': accessToken};
|
||||
if (refreshToken?.isNotEmpty == true) map['refresh_token'] = refreshToken;
|
||||
if (expiresIn != null) map['expires_in'] = expiresIn;
|
||||
if (scope != null) map['scope'] = scope.toList();
|
||||
return map;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +1,29 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:angel_framework/angel_framework.dart';
|
||||
import 'exception.dart';
|
||||
import 'response.dart';
|
||||
import 'token_type.dart';
|
||||
|
||||
String _getParam(RequestContext req, String name, {bool body: false}) {
|
||||
/// A request handler that performs an arbitrary authorization token grant.
|
||||
typedef Future<AuthorizationTokenResponse> ExtensionGrant(
|
||||
RequestContext req, ResponseContext res);
|
||||
|
||||
String _getParam(RequestContext req, String name, String state,
|
||||
{bool body: false}) {
|
||||
var map = body == true ? req.body : req.query;
|
||||
var value = map.containsKey(name) ? map[name]?.toString() : null;
|
||||
|
||||
if (value?.isNotEmpty != true)
|
||||
throw new AngelHttpException.badRequest(
|
||||
message: "Missing required parameter '$name'.");
|
||||
if (value?.isNotEmpty != true) {
|
||||
throw new AuthorizationException(
|
||||
new ErrorResponse(
|
||||
ErrorResponse.invalidRequest,
|
||||
'Missing required parameter "$name".',
|
||||
state,
|
||||
),
|
||||
statusCode: 400,
|
||||
);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
@ -20,100 +33,366 @@ Iterable<String> _getScopes(RequestContext req, {bool body: false}) {
|
|||
return map['scope']?.toString()?.split(' ') ?? [];
|
||||
}
|
||||
|
||||
abstract class Server<Client, User> {
|
||||
const Server();
|
||||
/// An OAuth2 authorization server, which issues access tokens to third parties.
|
||||
abstract class AuthorizationServer<Client, User> {
|
||||
const AuthorizationServer();
|
||||
|
||||
static const String _internalServerError =
|
||||
'An internal server error occurred.';
|
||||
|
||||
/// A [Map] of custom authorization token grants. Use this to handle custom grant types, perhaps even your own.
|
||||
Map<String, ExtensionGrant> get extensionGrants => {};
|
||||
|
||||
/// Finds the [Client] application associated with the given [clientId].
|
||||
FutureOr<Client> findClient(String clientId);
|
||||
|
||||
// TODO: Is this ever used???
|
||||
/// Verify that a [client] is the one identified by the [clientSecret].
|
||||
Future<bool> verifyClient(Client client, String clientSecret);
|
||||
|
||||
Future<String> authCodeGrant(Client client, String redirectUri, User user,
|
||||
Iterable<String> scopes, String state);
|
||||
/// Prompt the currently logged-in user to grant or deny access to the [client].
|
||||
///
|
||||
/// In many applications, this will entail showing a dialog to the user in question.
|
||||
requestAuthorizationCode(
|
||||
Client client,
|
||||
String redirectUri,
|
||||
Iterable<String> scopes,
|
||||
String state,
|
||||
RequestContext req,
|
||||
ResponseContext res) {
|
||||
throw new AuthorizationException(
|
||||
new ErrorResponse(
|
||||
ErrorResponse.unsupportedResponseType,
|
||||
'Authorization code grants are not supported.',
|
||||
state,
|
||||
),
|
||||
statusCode: 405,
|
||||
);
|
||||
}
|
||||
|
||||
authorize(Client client, String redirectUri, Iterable<String> scopes,
|
||||
String state, RequestContext req, ResponseContext res);
|
||||
/// Create an implicit authorization token.
|
||||
///
|
||||
/// Note that in cases where this is called, there is no guarantee
|
||||
/// that the user agent has not been compromised.
|
||||
Future<AuthorizationTokenResponse> implicitGrant(
|
||||
Client client,
|
||||
String redirectUri,
|
||||
Iterable<String> scopes,
|
||||
String state,
|
||||
RequestContext req,
|
||||
ResponseContext res) {
|
||||
throw new AuthorizationException(
|
||||
new ErrorResponse(
|
||||
ErrorResponse.unsupportedResponseType,
|
||||
'Authorization code grants are not supported.',
|
||||
state,
|
||||
),
|
||||
statusCode: 405,
|
||||
);
|
||||
}
|
||||
|
||||
Future<AuthorizationCodeResponse> exchangeAuthCodeForAccessToken(
|
||||
/// Exchanges an authorization code for an authorization token.
|
||||
Future<AuthorizationTokenResponse> exchangeAuthorizationCodeForToken(
|
||||
String authCode,
|
||||
String redirectUri,
|
||||
RequestContext req,
|
||||
ResponseContext res);
|
||||
ResponseContext res) {
|
||||
throw new AuthorizationException(
|
||||
new ErrorResponse(
|
||||
ErrorResponse.unsupportedResponseType,
|
||||
'Authorization code grants are not supported.',
|
||||
req.query['state'] ?? '',
|
||||
),
|
||||
statusCode: 405,
|
||||
);
|
||||
}
|
||||
|
||||
/// Refresh an authorization token.
|
||||
Future<AuthorizationTokenResponse> refreshAuthorizationToken(
|
||||
Client client,
|
||||
String refreshToken,
|
||||
Iterable<String> scopes,
|
||||
RequestContext req,
|
||||
ResponseContext res) {
|
||||
throw new AuthorizationException(
|
||||
new ErrorResponse(
|
||||
ErrorResponse.unsupportedResponseType,
|
||||
'Refreshing authorization tokens is not supported.',
|
||||
req.body['state'] ?? '',
|
||||
),
|
||||
statusCode: 405,
|
||||
);
|
||||
}
|
||||
|
||||
/// Issue an authorization token to a user after authenticating them via [username] and [password].
|
||||
Future<AuthorizationTokenResponse> resourceOwnerPasswordCredentialsGrant(
|
||||
Client client,
|
||||
String username,
|
||||
String password,
|
||||
Iterable<String> scopes,
|
||||
RequestContext req,
|
||||
ResponseContext res) {
|
||||
throw new AuthorizationException(
|
||||
new ErrorResponse(
|
||||
ErrorResponse.unsupportedResponseType,
|
||||
'Resource owner password credentials grants are not supported.',
|
||||
req.body['state'] ?? '',
|
||||
),
|
||||
statusCode: 405,
|
||||
);
|
||||
}
|
||||
|
||||
/// Performs a client credentials grant. Only use this in situations where the client is 100% trusted.
|
||||
Future<AuthorizationTokenResponse> clientCredentialsGrant(
|
||||
Client client, RequestContext req, ResponseContext res) {
|
||||
throw new AuthorizationException(
|
||||
new ErrorResponse(
|
||||
ErrorResponse.unsupportedResponseType,
|
||||
'Client credentials grants are not supported.',
|
||||
req.body['state'] ?? '',
|
||||
),
|
||||
statusCode: 405,
|
||||
);
|
||||
}
|
||||
|
||||
/// A request handler that invokes the correct logic, depending on which type
|
||||
/// of grant the client is requesting.
|
||||
Future authorizationEndpoint(RequestContext req, ResponseContext res) async {
|
||||
var responseType = _getParam(req, 'response_type');
|
||||
String state = '';
|
||||
|
||||
if (responseType != 'code')
|
||||
throw new AngelHttpException.badRequest(
|
||||
message: "Invalid response_type, expected 'code'.");
|
||||
try {
|
||||
state = req.query['state']?.toString() ?? '';
|
||||
var responseType = _getParam(req, 'response_type', state);
|
||||
|
||||
// Ensure client ID
|
||||
var clientId = _getParam(req, 'client_id');
|
||||
if (responseType == 'code') {
|
||||
// Ensure client ID
|
||||
// TODO: Handle confidential clients
|
||||
var clientId = _getParam(req, 'client_id', state);
|
||||
|
||||
// Find client
|
||||
var client = await findClient(clientId);
|
||||
// Find client
|
||||
var client = await findClient(clientId);
|
||||
|
||||
if (client == null)
|
||||
throw new AuthorizationException(ErrorResponse.invalidClient);
|
||||
if (client == null) {
|
||||
throw new AuthorizationException(new ErrorResponse(
|
||||
ErrorResponse.unauthorizedClient,
|
||||
'Unknown client "$clientId".',
|
||||
state,
|
||||
));
|
||||
}
|
||||
|
||||
// Grab redirect URI
|
||||
var redirectUri = _getParam(req, 'redirect_uri');
|
||||
// Grab redirect URI
|
||||
var redirectUri = _getParam(req, 'redirect_uri', state);
|
||||
|
||||
// Grab scopes
|
||||
var scopes = _getScopes(req);
|
||||
// Grab scopes
|
||||
var scopes = _getScopes(req);
|
||||
|
||||
var state = req.query['state']?.toString() ?? '';
|
||||
return await requestAuthorizationCode(
|
||||
client, redirectUri, scopes, state, req, res);
|
||||
}
|
||||
|
||||
return await authorize(client, redirectUri, scopes, state, req, res);
|
||||
if (responseType == 'token') {
|
||||
var clientId = _getParam(req, 'client_id', state);
|
||||
var client = await findClient(clientId);
|
||||
|
||||
if (client == null) {
|
||||
throw new AuthorizationException(new ErrorResponse(
|
||||
ErrorResponse.unauthorizedClient,
|
||||
'Unknown client "$clientId".',
|
||||
state,
|
||||
));
|
||||
}
|
||||
|
||||
var redirectUri = _getParam(req, 'redirect_uri', state);
|
||||
|
||||
// Grab scopes
|
||||
var scopes = _getScopes(req);
|
||||
var token =
|
||||
await implicitGrant(client, redirectUri, scopes, state, req, res);
|
||||
|
||||
Uri target;
|
||||
|
||||
try {
|
||||
target = Uri.parse(redirectUri);
|
||||
var queryParameters = <String, String>{};
|
||||
|
||||
queryParameters.addAll({
|
||||
'access_token': token.accessToken,
|
||||
'token_type': 'bearer',
|
||||
'state': state,
|
||||
});
|
||||
|
||||
if (token.expiresIn != null)
|
||||
queryParameters['expires_in'] = token.expiresIn.toString();
|
||||
|
||||
if (token.scope != null)
|
||||
queryParameters['scope'] = token.scope.join(' ');
|
||||
|
||||
var fragment = queryParameters.keys
|
||||
.fold<StringBuffer>(new StringBuffer(), (buf, k) {
|
||||
if (buf.isNotEmpty) buf.write('&');
|
||||
return buf
|
||||
..write(
|
||||
'$k=' + Uri.encodeComponent(queryParameters[k]),
|
||||
);
|
||||
}).toString();
|
||||
|
||||
target = target.replace(fragment: fragment);
|
||||
res.redirect(target.toString());
|
||||
return false;
|
||||
} on FormatException {
|
||||
throw new AuthorizationException(
|
||||
new ErrorResponse(
|
||||
ErrorResponse.invalidRequest,
|
||||
'Invalid URI provided as "redirect_uri" parameter',
|
||||
state,
|
||||
),
|
||||
statusCode: 400);
|
||||
}
|
||||
}
|
||||
|
||||
throw new AuthorizationException(
|
||||
new ErrorResponse(
|
||||
ErrorResponse.invalidRequest,
|
||||
'Invalid or no "response_type" parameter provided',
|
||||
state,
|
||||
),
|
||||
statusCode: 400);
|
||||
} on AngelHttpException {
|
||||
rethrow;
|
||||
} catch (e, st) {
|
||||
throw new AuthorizationException(
|
||||
new ErrorResponse(
|
||||
ErrorResponse.serverError,
|
||||
_internalServerError,
|
||||
state,
|
||||
),
|
||||
error: e,
|
||||
statusCode: 500,
|
||||
stackTrace: st,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static final RegExp _rgxBasic = new RegExp(r'Basic ([^$]+)');
|
||||
static final RegExp _rgxBasicAuth = new RegExp(r'([^:]*):([^$]*)');
|
||||
|
||||
/// A request handler that either exchanges authorization codes for authorization tokens,
|
||||
/// or refreshes authorization tokens.
|
||||
Future tokenEndpoint(RequestContext req, ResponseContext res) async {
|
||||
await req.parse();
|
||||
String state = '';
|
||||
Client client;
|
||||
|
||||
var grantType = _getParam(req, 'grant_type', body: true);
|
||||
try {
|
||||
AuthorizationTokenResponse response;
|
||||
await req.parse();
|
||||
|
||||
if (grantType != 'authorization_code')
|
||||
throw new AngelHttpException.badRequest(
|
||||
message: "Invalid grant_type; expected 'authorization_code'.");
|
||||
state = req.body['state'] ?? '';
|
||||
|
||||
var code = _getParam(req, 'code', body: true);
|
||||
var redirectUri = _getParam(req, 'redirect_uri', body: true);
|
||||
var grantType = _getParam(req, 'grant_type', state, body: true);
|
||||
|
||||
var response =
|
||||
await exchangeAuthCodeForAccessToken(code, redirectUri, req, res);
|
||||
return {'token_type': TokenType.bearer}..addAll(response.toJson());
|
||||
}
|
||||
if (grantType != 'authorization_code') {
|
||||
var match =
|
||||
_rgxBasic.firstMatch(req.headers.value('authorization') ?? '');
|
||||
|
||||
Future handleFormSubmission(RequestContext req, ResponseContext res) async {
|
||||
await req.parse();
|
||||
if (match != null) {
|
||||
match = _rgxBasicAuth
|
||||
.firstMatch(new String.fromCharCodes(BASE64URL.decode(match[1])));
|
||||
}
|
||||
|
||||
// Ensure client ID
|
||||
var clientId = _getParam(req, 'client_id', body: true);
|
||||
if (match == null) {
|
||||
throw new AuthorizationException(
|
||||
new ErrorResponse(
|
||||
ErrorResponse.unauthorizedClient,
|
||||
'Invalid or no "Authorization" header.',
|
||||
state,
|
||||
),
|
||||
statusCode: 400,
|
||||
);
|
||||
} else {
|
||||
var clientId = match[1], clientSecret = match[2];
|
||||
client = await findClient(clientId);
|
||||
|
||||
// Find client
|
||||
var client = await findClient(clientId);
|
||||
if (client == null) {
|
||||
throw new AuthorizationException(
|
||||
new ErrorResponse(
|
||||
ErrorResponse.unauthorizedClient,
|
||||
'Invalid "client_id" parameter.',
|
||||
state,
|
||||
),
|
||||
statusCode: 401,
|
||||
);
|
||||
}
|
||||
|
||||
if (client == null)
|
||||
throw new AuthorizationException(ErrorResponse.invalidClient);
|
||||
if (!await verifyClient(client, clientSecret)) {
|
||||
throw new AuthorizationException(
|
||||
new ErrorResponse(
|
||||
ErrorResponse.unauthorizedClient,
|
||||
'Invalid "client_secret" parameter.',
|
||||
state,
|
||||
),
|
||||
statusCode: 401,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Verify client secret
|
||||
var clientSecret = _getParam(req, 'client_secret', body: true);
|
||||
if (grantType == 'authorization_code') {
|
||||
var code = _getParam(req, 'code', state, body: true);
|
||||
var redirectUri = _getParam(req, 'redirect_uri', state, body: true);
|
||||
response = await exchangeAuthorizationCodeForToken(
|
||||
code, redirectUri, req, res);
|
||||
} else if (grantType == 'refresh_token') {
|
||||
var refreshToken = _getParam(req, 'refresh_token', state, body: true);
|
||||
var scopes = _getScopes(req);
|
||||
response = await refreshAuthorizationToken(
|
||||
client, refreshToken, scopes, req, res);
|
||||
} else if (grantType == 'password') {
|
||||
var username = _getParam(req, 'username', state, body: true);
|
||||
var password = _getParam(req, 'password', state, body: true);
|
||||
var scopes = _getScopes(req);
|
||||
response = await resourceOwnerPasswordCredentialsGrant(
|
||||
client, username, password, scopes, req, res);
|
||||
} else if (grantType == 'client_credentials') {
|
||||
response = await clientCredentialsGrant(client, req, res);
|
||||
|
||||
if (!await verifyClient(client, clientSecret))
|
||||
throw new AuthorizationException(ErrorResponse.invalidClient);
|
||||
if (response.refreshToken != null) {
|
||||
// Remove refresh token
|
||||
response = new AuthorizationTokenResponse(
|
||||
response.accessToken,
|
||||
expiresIn: response.expiresIn,
|
||||
scope: response.scope,
|
||||
);
|
||||
}
|
||||
} else if (extensionGrants.containsKey(grantType)) {
|
||||
response = await extensionGrants[grantType](req, res);
|
||||
}
|
||||
|
||||
// Grab redirect URI
|
||||
var redirectUri = _getParam(req, 'redirect_uri', body: true);
|
||||
if (response != null) {
|
||||
return {'token_type': AuthorizationTokenType.bearer}
|
||||
..addAll(response.toJson());
|
||||
}
|
||||
|
||||
// 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));
|
||||
throw new AuthorizationException(
|
||||
new ErrorResponse(
|
||||
ErrorResponse.invalidRequest,
|
||||
'Invalid or no "grant_type" parameter provided',
|
||||
state,
|
||||
),
|
||||
statusCode: 400,
|
||||
);
|
||||
} on AngelHttpException {
|
||||
rethrow;
|
||||
} catch (e, st) {
|
||||
throw new AuthorizationException(
|
||||
new ErrorResponse(
|
||||
ErrorResponse.serverError,
|
||||
_internalServerError,
|
||||
state,
|
||||
),
|
||||
error: e,
|
||||
statusCode: 500,
|
||||
stackTrace: st,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
import 'package:angel_auth/angel_auth.dart';
|
||||
import 'server.dart';
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
abstract class TokenType {
|
||||
/// The various types of OAuth2 authorization tokens.
|
||||
abstract class AuthorizationTokenType {
|
||||
static const String bearer = 'bearer', mac = 'mac';
|
||||
}
|
|
@ -6,7 +6,7 @@ version: 1.0.0-alpha
|
|||
environment:
|
||||
sdk: ">=1.19.0"
|
||||
dependencies:
|
||||
angel_auth: ^1.1.0-alpha
|
||||
angel_framework: ^1.0.0-dev
|
||||
dev_dependencies:
|
||||
angel_test: ^1.1.0-alpha
|
||||
oauth2: ^1.0.0
|
||||
|
|
|
@ -115,7 +115,7 @@ main() {
|
|||
});
|
||||
}
|
||||
|
||||
class _Server extends Server<PseudoApplication, Map> {
|
||||
class _Server extends AuthorizationServer<PseudoApplication, Map> {
|
||||
final Uuid _uuid = new Uuid();
|
||||
|
||||
@override
|
||||
|
@ -130,7 +130,7 @@ class _Server extends Server<PseudoApplication, Map> {
|
|||
}
|
||||
|
||||
@override
|
||||
Future authorize(
|
||||
Future requestAuthorizationCode(
|
||||
PseudoApplication client,
|
||||
String redirectUri,
|
||||
Iterable<String> scopes,
|
||||
|
@ -151,13 +151,7 @@ class _Server extends Server<PseudoApplication, Map> {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<String> authCodeGrant(PseudoApplication client, String redirectUri,
|
||||
Map user, Iterable<String> scopes, String state) {
|
||||
throw new UnsupportedError('Nope');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<AuthorizationCodeResponse> exchangeAuthCodeForAccessToken(
|
||||
Future<AuthorizationTokenResponse> exchangeAuthorizationCodeForToken(
|
||||
String authCode,
|
||||
String redirectUri,
|
||||
RequestContext req,
|
||||
|
@ -165,7 +159,7 @@ class _Server extends Server<PseudoApplication, Map> {
|
|||
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',
|
||||
return new AuthorizationTokenResponse('${authCode}_access',
|
||||
refreshToken: refreshToken);
|
||||
}
|
||||
}
|
||||
|
|
105
test/client_credentials_test.dart
Normal file
105
test/client_credentials_test.dart
Normal file
|
@ -0,0 +1,105 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:angel_framework/angel_framework.dart';
|
||||
import 'package:angel_test/angel_test.dart';
|
||||
import 'package:angel_oauth2/angel_oauth2.dart';
|
||||
import 'package:angel_validate/angel_validate.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'common.dart';
|
||||
|
||||
main() {
|
||||
TestClient client;
|
||||
|
||||
setUp(() async {
|
||||
var app = new Angel()..lazyParseBodies = true;
|
||||
var oauth2 = new _AuthorizationServer();
|
||||
|
||||
app.group('/oauth2', (router) {
|
||||
router
|
||||
..get('/authorize', oauth2.authorizationEndpoint)
|
||||
..post('/token', oauth2.tokenEndpoint);
|
||||
});
|
||||
|
||||
app.errorHandler = (e, req, res) async {
|
||||
res.json(e.toJson());
|
||||
};
|
||||
|
||||
client = await connectTo(app);
|
||||
});
|
||||
|
||||
tearDown(() => client.close());
|
||||
|
||||
test('authenticate via client credentials', () async {
|
||||
var response = await client.post(
|
||||
'/oauth2/token',
|
||||
headers: {
|
||||
'Authorization': 'Basic ' + BASE64URL.encode('foo:bar'.codeUnits),
|
||||
},
|
||||
body: {
|
||||
'grant_type': 'client_credentials',
|
||||
},
|
||||
);
|
||||
|
||||
print('Response: ${response.body}');
|
||||
|
||||
expect(response, allOf(
|
||||
hasStatus(200),
|
||||
hasContentType(ContentType.JSON),
|
||||
hasValidBody(new Validator({
|
||||
'token_type': equals('bearer'),
|
||||
'access_token': equals('foo'),
|
||||
})),
|
||||
));
|
||||
});
|
||||
|
||||
test('force correct id', () async {
|
||||
var response = await client.post(
|
||||
'/oauth2/token',
|
||||
headers: {
|
||||
'Authorization': 'Basic ' + BASE64URL.encode('fooa:bar'.codeUnits),
|
||||
},
|
||||
body: {
|
||||
'grant_type': 'client_credentials',
|
||||
},
|
||||
);
|
||||
|
||||
print('Response: ${response.body}');
|
||||
expect(response, hasStatus(401));
|
||||
});
|
||||
|
||||
test('force correct secret', () async {
|
||||
var response = await client.post(
|
||||
'/oauth2/token',
|
||||
headers: {
|
||||
'Authorization': 'Basic ' + BASE64URL.encode('foo:bara'.codeUnits),
|
||||
},
|
||||
body: {
|
||||
'grant_type': 'client_credentials',
|
||||
},
|
||||
);
|
||||
|
||||
print('Response: ${response.body}');
|
||||
expect(response, hasStatus(401));
|
||||
});
|
||||
}
|
||||
|
||||
class _AuthorizationServer
|
||||
extends AuthorizationServer<PseudoApplication, PseudoUser> {
|
||||
@override
|
||||
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<AuthorizationTokenResponse> clientCredentialsGrant(
|
||||
PseudoApplication client, RequestContext req, ResponseContext res) async {
|
||||
return new AuthorizationTokenResponse('foo');
|
||||
}
|
||||
}
|
|
@ -6,3 +6,15 @@ class PseudoApplication {
|
|||
|
||||
const PseudoApplication(this.id, this.secret, this.redirectUri);
|
||||
}
|
||||
|
||||
const List<PseudoUser> pseudoUsers = const [
|
||||
const PseudoUser(username: 'foo', password: 'bar'),
|
||||
const PseudoUser(username: 'michael', password: 'jackson'),
|
||||
const PseudoUser(username: 'jon', password: 'skeet'),
|
||||
];
|
||||
|
||||
class PseudoUser {
|
||||
final String username, password;
|
||||
|
||||
const PseudoUser({this.username, this.password});
|
||||
}
|
69
test/implicit_grant_test.dart
Normal file
69
test/implicit_grant_test.dart
Normal file
|
@ -0,0 +1,69 @@
|
|||
import 'dart:async';
|
||||
import 'package:angel_framework/angel_framework.dart';
|
||||
import 'package:angel_test/angel_test.dart';
|
||||
import 'package:angel_oauth2/angel_oauth2.dart';
|
||||
import 'package:angel_validate/angel_validate.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'common.dart';
|
||||
|
||||
main() {
|
||||
TestClient client;
|
||||
|
||||
setUp(() async {
|
||||
var app = new Angel()..lazyParseBodies = true;
|
||||
var oauth2 = new _AuthorizationServer();
|
||||
|
||||
app.group('/oauth2', (router) {
|
||||
router
|
||||
..get('/authorize', oauth2.authorizationEndpoint)
|
||||
..post('/token', oauth2.tokenEndpoint);
|
||||
});
|
||||
|
||||
app.errorHandler = (e, req, res) async {
|
||||
res.json(e.toJson());
|
||||
};
|
||||
|
||||
client = await connectTo(app);
|
||||
});
|
||||
|
||||
tearDown(() => client.close());
|
||||
|
||||
test('authenticate via implicit grant', () async {
|
||||
var response = await client.get(
|
||||
'/oauth2/authorize?response_type=token&client_id=foo&redirect_uri=http://foo.com&state=bar',
|
||||
);
|
||||
|
||||
print('Headers: ${response.headers}');
|
||||
expect(
|
||||
response,
|
||||
allOf(
|
||||
hasStatus(302),
|
||||
hasHeader('location', 'http://foo.com#access_token=foo&token_type=bearer&state=bar'),
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
class _AuthorizationServer
|
||||
extends AuthorizationServer<PseudoApplication, PseudoUser> {
|
||||
@override
|
||||
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<AuthorizationTokenResponse> implicitGrant(
|
||||
PseudoApplication client,
|
||||
String redirectUri,
|
||||
Iterable<String> scopes,
|
||||
String state,
|
||||
RequestContext req,
|
||||
ResponseContext res) async {
|
||||
return new AuthorizationTokenResponse('foo');
|
||||
}
|
||||
}
|
114
test/password_test.dart
Normal file
114
test/password_test.dart
Normal file
|
@ -0,0 +1,114 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:angel_framework/angel_framework.dart';
|
||||
import 'package:angel_test/angel_test.dart';
|
||||
import 'package:angel_oauth2/angel_oauth2.dart';
|
||||
import 'package:angel_validate/angel_validate.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'common.dart';
|
||||
|
||||
main() {
|
||||
TestClient client;
|
||||
|
||||
setUp(() async {
|
||||
var app = new Angel()..lazyParseBodies = true;
|
||||
var oauth2 = new _AuthorizationServer();
|
||||
|
||||
app.group('/oauth2', (router) {
|
||||
router
|
||||
..get('/authorize', oauth2.authorizationEndpoint)
|
||||
..post('/token', oauth2.tokenEndpoint);
|
||||
});
|
||||
|
||||
app.errorHandler = (e, req, res) async {
|
||||
res.json(e.toJson());
|
||||
};
|
||||
|
||||
client = await connectTo(app);
|
||||
});
|
||||
|
||||
tearDown(() => client.close());
|
||||
|
||||
test('authenticate via username+password', () async {
|
||||
var response = await client.post(
|
||||
'/oauth2/token',
|
||||
headers: {
|
||||
'Authorization': 'Basic ' + BASE64URL.encode('foo:bar'.codeUnits),
|
||||
},
|
||||
body: {
|
||||
'grant_type': 'password',
|
||||
'username': 'michael',
|
||||
'password': 'jackson',
|
||||
},
|
||||
);
|
||||
|
||||
print('Response: ${response.body}');
|
||||
|
||||
expect(response, allOf(
|
||||
hasStatus(200),
|
||||
hasContentType(ContentType.JSON),
|
||||
hasValidBody(new Validator({
|
||||
'token_type': equals('bearer'),
|
||||
'access_token': equals('foo'),
|
||||
})),
|
||||
));
|
||||
});
|
||||
|
||||
test('force correct username+password', () async {
|
||||
var response = await client.post(
|
||||
'/oauth2/token',
|
||||
headers: {
|
||||
'Authorization': 'Basic ' + BASE64URL.encode('foo:bar'.codeUnits),
|
||||
},
|
||||
body: {
|
||||
'grant_type': 'password',
|
||||
'username': 'michael',
|
||||
'password': 'jordan',
|
||||
},
|
||||
);
|
||||
|
||||
print('Response: ${response.body}');
|
||||
expect(response, hasStatus(401));
|
||||
});
|
||||
}
|
||||
|
||||
class _AuthorizationServer
|
||||
extends AuthorizationServer<PseudoApplication, PseudoUser> {
|
||||
@override
|
||||
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<AuthorizationTokenResponse> resourceOwnerPasswordCredentialsGrant(
|
||||
PseudoApplication client,
|
||||
String username,
|
||||
String password,
|
||||
Iterable<String> scopes,
|
||||
RequestContext req,
|
||||
ResponseContext res) async {
|
||||
var user = pseudoUsers.firstWhere(
|
||||
(u) => u.username == username && u.password == password,
|
||||
orElse: () => null);
|
||||
|
||||
if (user == null) {
|
||||
throw new AuthorizationException(
|
||||
new ErrorResponse(
|
||||
ErrorResponse.accessDenied,
|
||||
'Invalid username or password.',
|
||||
req.body['state'] ?? '',
|
||||
),
|
||||
statusCode: 401,
|
||||
);
|
||||
}
|
||||
|
||||
return new AuthorizationTokenResponse('foo');
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue