1.0.5
This commit is contained in:
parent
b00329f632
commit
71a6479b41
6 changed files with 185 additions and 72 deletions
|
@ -13,7 +13,6 @@
|
|||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
<orderEntry type="library" name="Dart SDK" level="project" />
|
||||
<orderEntry type="library" name="Dart SDK" level="application" />
|
||||
<orderEntry type="library" name="Dart Packages" level="project" />
|
||||
</component>
|
||||
</module>
|
54
README.md
54
README.md
|
@ -1,15 +1,43 @@
|
|||
# angel_auth
|
||||
|
||||
[![version 1.0.4+1](https://img.shields.io/badge/version-1.0.4+1-brightgreen.svg)](https://pub.dartlang.org/packages/angel_auth)
|
||||
[![version 1.0.5](https://img.shields.io/badge/version-1.0.5-brightgreen.svg)](https://pub.dartlang.org/packages/angel_auth)
|
||||
![build status](https://travis-ci.org/angel-dart/auth.svg?branch=master)
|
||||
|
||||
A complete authentication plugin for Angel. Inspired by Passport.
|
||||
|
||||
# Documentation
|
||||
# Wiki
|
||||
[Click here](https://github.com/angel-dart/auth/wiki).
|
||||
|
||||
# Supported Strategies
|
||||
# Bundled Strategies
|
||||
* Local (with and without Basic Auth)
|
||||
* Find other strategies (Twitter, Google, OAuth2, etc.) on Pub!!!
|
||||
|
||||
# Example
|
||||
Ensure you have read the [wiki](https://github.com/angel-dart/auth/wiki).
|
||||
|
||||
```dart
|
||||
configureServer(Angel app) async {
|
||||
var auth = new AngelAuth();
|
||||
auth.serializer = ...;
|
||||
auth.deserializer = ...;
|
||||
auth.strategies.add(new LocalAuthStrategy(...));
|
||||
|
||||
// POST route to handle username+password
|
||||
app.post('/local', auth.authenticate('local'));
|
||||
|
||||
// Use a comma to try multiple strategies!!!
|
||||
//
|
||||
// Each strategy is run sequentially. If one succeeds, the loop ends.
|
||||
// Authentication failures will just cause the loop to continue.
|
||||
//
|
||||
// If the last strategy throws an authentication failure, then
|
||||
// a `401 Not Authenticated` is thrown.
|
||||
var chainedHandler = auth.authenticate(
|
||||
'basic,facebook',
|
||||
authOptions
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
# Default Authentication Callback
|
||||
A frequent use case within SPA's is opening OAuth login endpoints in a separate window.
|
||||
|
@ -18,8 +46,26 @@ provides a facility for this, which works perfectly with the default callback pr
|
|||
in this package.
|
||||
|
||||
```dart
|
||||
auth.authenticate('facebook', new AngelAuthOptions(callback: confirmPopupAuthentication()));
|
||||
configureServer(Angel app) async {
|
||||
var handler = auth.authenticate(
|
||||
'facebook',
|
||||
new AngelAuthOptions(callback: confirmPopupAuthentication()));
|
||||
app.get('/auth/facebook', handler);
|
||||
|
||||
// Use a comma to try multiple strategies!!!
|
||||
//
|
||||
// Each strategy is run sequentially. If one succeeds, the loop ends.
|
||||
// Authentication failures will just cause the loop to continue.
|
||||
//
|
||||
// If the last strategy throws an authentication failure, then
|
||||
// a `401 Not Authenticated` is thrown.
|
||||
var chainedHandler = auth.authenticate(
|
||||
'basic,facebook',
|
||||
authOptions
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
This renders a simple HTML page that fires the user's JWT as a `token` event in `window.opener`.
|
||||
`angel_client` [exposes this as a Stream](https://github.com/angel-dart/client#authentication):
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import 'dart:async';
|
||||
|
||||
/// Serializes a user to the session.
|
||||
typedef Future UserSerializer(user);
|
||||
typedef FutureOr UserSerializer<T>(T user);
|
||||
|
||||
/// Deserializes a user from the session.
|
||||
typedef Future UserDeserializer(userId);
|
||||
typedef FutureOr<T> UserDeserializer<T>(userId);
|
|
@ -9,21 +9,50 @@ import 'defs.dart';
|
|||
import 'options.dart';
|
||||
import 'strategy.dart';
|
||||
|
||||
class AngelAuth extends AngelPlugin {
|
||||
/// Handles authentication within an Angel application.
|
||||
class AngelAuth<T> extends AngelPlugin {
|
||||
Hmac _hs256;
|
||||
num _jwtLifeSpan;
|
||||
final StreamController<T> _onLogin = new StreamController<T>(),
|
||||
_onLogout = new StreamController<T>();
|
||||
Math.Random _random = new Math.Random.secure();
|
||||
final RegExp _rgxBearer = new RegExp(r"^Bearer");
|
||||
final bool allowCookie;
|
||||
final bool allowTokenInQuery;
|
||||
String middlewareName;
|
||||
bool debug;
|
||||
bool enforceIp;
|
||||
String reviveTokenEndpoint;
|
||||
List<AuthStrategy> strategies = [];
|
||||
UserSerializer serializer;
|
||||
UserDeserializer deserializer;
|
||||
|
||||
/// If `true` (default), then JWT's will be stored and retrieved from a `token` cookie.
|
||||
final bool allowCookie;
|
||||
|
||||
/// If `true` (default), then users can include a JWT in the query string as `token`.
|
||||
final bool allowTokenInQuery;
|
||||
|
||||
/// The name to register [requireAuth] as. Default: `auth`.
|
||||
String middlewareName;
|
||||
|
||||
bool debug;
|
||||
|
||||
/// If `true` (default), then JWT's will be considered invalid if used from a different IP than the first user's it was issued to.
|
||||
///
|
||||
/// This is a security provision. Even if a user's JWT is stolen, a remote attacker will not be able to impersonate anyone.
|
||||
bool enforceIp;
|
||||
|
||||
/// The endpoint to mount [reviveJwt] at. If `null`, then no revival route is mounted. Default: `/auth/token`.
|
||||
String reviveTokenEndpoint;
|
||||
|
||||
/// A set of [AuthStrategy] instances used to authenticate users.
|
||||
List<AuthStrategy> strategies = [];
|
||||
|
||||
/// Serializes a user into a unique identifier associated only with one identity.
|
||||
UserSerializer<T> serializer;
|
||||
|
||||
/// Deserializes a unique identifier into its associated identity. In most cases, this is a user object or model instance.
|
||||
UserDeserializer<T> deserializer;
|
||||
|
||||
/// Fires the result of [deserializer] whenever a user signs in to the application.
|
||||
Stream<T> get onLogin => _onLogin.stream;
|
||||
|
||||
/// Fires `req.user`, which is usually the result of [deserializer], whenever a user signs out of the application.
|
||||
Stream<T> get onLogout => _onLogout.stream;
|
||||
|
||||
/// The [Hmac] being used to encode JWT's.
|
||||
Hmac get hmac => _hs256;
|
||||
|
||||
String _randomString(
|
||||
|
@ -31,9 +60,7 @@ class AngelAuth extends AngelPlugin {
|
|||
String validChars:
|
||||
"ABCDEFHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_"}) {
|
||||
var chars = <int>[];
|
||||
|
||||
while (chars.length < length) chars.add(_random.nextInt(validChars.length));
|
||||
|
||||
return new String.fromCharCodes(chars);
|
||||
}
|
||||
|
||||
|
@ -53,6 +80,13 @@ class AngelAuth extends AngelPlugin {
|
|||
|
||||
@override
|
||||
call(Angel app) async {
|
||||
if (serializer == null)
|
||||
throw new StateError(
|
||||
'An `AngelAuth` plug-in was called without its `serializer` being set. All authentication will fail.');
|
||||
if (deserializer == null)
|
||||
throw new StateError(
|
||||
'An `AngelAuth` plug-in was called without its `deserializer` being set. All authentication will fail.');
|
||||
|
||||
app.container.singleton(this);
|
||||
if (runtimeType != AngelAuth) app.container.singleton(this, as: AngelAuth);
|
||||
|
||||
|
@ -62,6 +96,10 @@ class AngelAuth extends AngelPlugin {
|
|||
if (reviveTokenEndpoint != null) {
|
||||
app.post(reviveTokenEndpoint, reviveJwt);
|
||||
}
|
||||
|
||||
app.justBeforeStop.add((_) {
|
||||
_onLogin.close();
|
||||
});
|
||||
}
|
||||
|
||||
void _apply(RequestContext req, AuthToken token, user) {
|
||||
|
@ -70,6 +108,7 @@ class AngelAuth extends AngelPlugin {
|
|||
..inject(user.runtimeType, req.properties["user"] = user);
|
||||
}
|
||||
|
||||
/// A middleware that decodes a JWT from a request, and injects a corresponding user.
|
||||
decodeJwt(RequestContext req, ResponseContext res) async {
|
||||
if (req.method == "POST" && req.path == reviveTokenEndpoint) {
|
||||
// Shouldn't block invalid JWT if we are reviving it
|
||||
|
@ -129,6 +168,7 @@ class AngelAuth extends AngelPlugin {
|
|||
return true;
|
||||
}
|
||||
|
||||
/// Retrieves a JWT from a request, if any was sent at all.
|
||||
getJwt(RequestContext req) {
|
||||
if (debug) {
|
||||
print('Attempting to parse JWT');
|
||||
|
@ -155,6 +195,7 @@ class AngelAuth extends AngelPlugin {
|
|||
return null;
|
||||
}
|
||||
|
||||
/// Attempts to revive an expired (or still alive) JWT.
|
||||
reviveJwt(RequestContext req, ResponseContext res) async {
|
||||
try {
|
||||
if (debug) print('Attempting to revive JWT...');
|
||||
|
@ -228,68 +269,85 @@ class AngelAuth extends AngelPlugin {
|
|||
}
|
||||
}
|
||||
|
||||
/// Attempts to authenticate a user using one or more strategies.
|
||||
///
|
||||
/// [type] is a comma-separated list of strategy names to try.
|
||||
///
|
||||
/// If a strategy returns `null` or `false`, either the next one is tried,
|
||||
/// or a `401 Not Authenticated` is thrown, if it is the last one.
|
||||
///
|
||||
/// Any other result is considered an authenticated user, and terminates the loop.
|
||||
authenticate(String type, [AngelAuthOptions options]) {
|
||||
return (RequestContext req, ResponseContext res) async {
|
||||
AuthStrategy strategy =
|
||||
strategies.firstWhere((AuthStrategy x) => x.name == type);
|
||||
var result = await strategy.authenticate(req, res, options);
|
||||
if (result == true)
|
||||
return result;
|
||||
else if (result != false) {
|
||||
var userId = await serializer(result);
|
||||
var names = type
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.where((String s) => s.isNotEmpty)
|
||||
.toList();
|
||||
|
||||
// Create JWT
|
||||
var token = new AuthToken(
|
||||
userId: userId, lifeSpan: _jwtLifeSpan, ipAddress: req.ip);
|
||||
var jwt = token.serialize(_hs256);
|
||||
for (int i = 0; i < names.length; i++) {
|
||||
var name = names[i];
|
||||
|
||||
if (options?.tokenCallback != null) {
|
||||
var r = await options.tokenCallback(
|
||||
req, res, token, req.properties["user"] = result);
|
||||
if (r != null) return r;
|
||||
AuthStrategy strategy =
|
||||
strategies.firstWhere((AuthStrategy x) => x.name == name);
|
||||
var result = await strategy.authenticate(req, res, options);
|
||||
if (result == true)
|
||||
return result;
|
||||
else if (result != false) {
|
||||
var userId = await serializer(result);
|
||||
|
||||
// Create JWT
|
||||
var token = new AuthToken(
|
||||
userId: userId, lifeSpan: _jwtLifeSpan, ipAddress: req.ip);
|
||||
var jwt = token.serialize(_hs256);
|
||||
|
||||
if (options?.tokenCallback != null) {
|
||||
var r = await options.tokenCallback(
|
||||
req, res, token, req.properties["user"] = result);
|
||||
if (r != null) return r;
|
||||
}
|
||||
|
||||
_apply(req, token, result);
|
||||
|
||||
if (allowCookie) res.cookies.add(new Cookie("token", jwt));
|
||||
|
||||
if (options?.callback != null) {
|
||||
return await options.callback(req, res, jwt);
|
||||
}
|
||||
|
||||
if (options?.successRedirect?.isNotEmpty == true) {
|
||||
res.redirect(options.successRedirect, code: HttpStatus.OK);
|
||||
return false;
|
||||
} else if (options?.canRespondWithJson != false &&
|
||||
req.headers.value("accept") != null &&
|
||||
(req.headers.value("accept").contains("application/json") ||
|
||||
req.headers.value("accept").contains("*/*") ||
|
||||
req.headers.value("accept").contains("application/*"))) {
|
||||
var user = await deserializer(await serializer(result));
|
||||
_onLogin.add(user);
|
||||
return {"data": user, "token": jwt};
|
||||
}
|
||||
|
||||
return true;
|
||||
} else {
|
||||
if (i < names.length - 1) continue;
|
||||
// Check if not redirect
|
||||
if (res.statusCode == 301 ||
|
||||
res.statusCode == 302 ||
|
||||
res.headers.containsKey(HttpHeaders.LOCATION))
|
||||
return false;
|
||||
else
|
||||
throw new AngelHttpException.notAuthenticated();
|
||||
}
|
||||
|
||||
_apply(req, token, result);
|
||||
|
||||
if (allowCookie) res.cookies.add(new Cookie("token", jwt));
|
||||
|
||||
if (options?.callback != null) {
|
||||
return await options.callback(req, res, jwt);
|
||||
}
|
||||
|
||||
if (options?.successRedirect?.isNotEmpty == true) {
|
||||
res.redirect(options.successRedirect, code: HttpStatus.OK);
|
||||
return false;
|
||||
} else if (options?.canRespondWithJson != false &&
|
||||
req.headers.value("accept") != null &&
|
||||
(req.headers.value("accept").contains("application/json") ||
|
||||
req.headers.value("accept").contains("*/*") ||
|
||||
req.headers.value("accept").contains("application/*"))) {
|
||||
var user = await deserializer(await serializer(result));
|
||||
return {"data": user, "token": jwt};
|
||||
}
|
||||
|
||||
return true;
|
||||
} else {
|
||||
// Check if not redirect
|
||||
if (res.statusCode == 301 ||
|
||||
res.statusCode == 302 ||
|
||||
res.headers.containsKey(HttpHeaders.LOCATION))
|
||||
return false;
|
||||
else
|
||||
await authenticationFailure(req, res);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Future authenticationFailure(RequestContext req, ResponseContext res) async {
|
||||
throw new AngelHttpException.notAuthenticated();
|
||||
}
|
||||
|
||||
/// Log a user in on-demand.
|
||||
Future login(AuthToken token, RequestContext req, ResponseContext res) async {
|
||||
var user = await deserializer(token.userId);
|
||||
_apply(req, token, user);
|
||||
_onLogin.add(user);
|
||||
|
||||
if (allowCookie)
|
||||
res.cookies.add(new Cookie('token', token.serialize(_hs256)));
|
||||
|
@ -301,11 +359,13 @@ class AngelAuth extends AngelPlugin {
|
|||
var token = new AuthToken(
|
||||
userId: userId, lifeSpan: _jwtLifeSpan, ipAddress: req.ip);
|
||||
_apply(req, token, user);
|
||||
_onLogin.add(user);
|
||||
|
||||
if (allowCookie)
|
||||
res.cookies.add(new Cookie('token', token.serialize(_hs256)));
|
||||
}
|
||||
|
||||
/// Log an authenticated user out.
|
||||
logout([AngelAuthOptions options]) {
|
||||
return (RequestContext req, ResponseContext res) async {
|
||||
for (AuthStrategy strategy in strategies) {
|
||||
|
@ -320,7 +380,14 @@ class AngelAuth extends AngelPlugin {
|
|||
}
|
||||
}
|
||||
|
||||
res.cookies.removeWhere((cookie) => cookie.name == "token");
|
||||
var user = req.grab('user');
|
||||
if (user != null) _onLogout.add(user);
|
||||
|
||||
req.injections..remove(AuthToken)..remove('user');
|
||||
req.properties.remove('user');
|
||||
|
||||
if (allowCookie == true)
|
||||
res.cookies.removeWhere((cookie) => cookie.name == "token");
|
||||
|
||||
if (options != null &&
|
||||
options.successRedirect != null &&
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
name: angel_auth
|
||||
description: A complete authentication plugin for Angel.
|
||||
version: 1.0.4+1
|
||||
version: 1.0.5
|
||||
author: Tobe O <thosakwe@gmail.com>
|
||||
homepage: https://github.com/angel-dart/angel_auth
|
||||
environment:
|
||||
|
|
|
@ -26,11 +26,12 @@ main() {
|
|||
.service('users')
|
||||
.create({'username': 'jdoe1', 'password': 'password'});
|
||||
|
||||
await app.configure(auth = new AngelAuth());
|
||||
|
||||
auth = new AngelAuth();
|
||||
auth.serializer = (User user) async => user.id;
|
||||
auth.deserializer = app.service('users').read;
|
||||
|
||||
await app.configure(auth);
|
||||
|
||||
auth.strategies.add(new LocalAuthStrategy((username, password) async {
|
||||
final List<User> users = await app.service('users').index();
|
||||
final found = users.firstWhere(
|
||||
|
|
Loading…
Reference in a new issue