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="inheritedJdk" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
<orderEntry type="library" name="Dart SDK" level="project" />
|
<orderEntry type="library" name="Dart SDK" level="project" />
|
||||||
<orderEntry type="library" name="Dart SDK" level="application" />
|
|
||||||
<orderEntry type="library" name="Dart Packages" level="project" />
|
<orderEntry type="library" name="Dart Packages" level="project" />
|
||||||
</component>
|
</component>
|
||||||
</module>
|
</module>
|
54
README.md
54
README.md
|
@ -1,15 +1,43 @@
|
||||||
# angel_auth
|
# 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)
|
![build status](https://travis-ci.org/angel-dart/auth.svg?branch=master)
|
||||||
|
|
||||||
A complete authentication plugin for Angel. Inspired by Passport.
|
A complete authentication plugin for Angel. Inspired by Passport.
|
||||||
|
|
||||||
# Documentation
|
# Wiki
|
||||||
[Click here](https://github.com/angel-dart/auth/wiki).
|
[Click here](https://github.com/angel-dart/auth/wiki).
|
||||||
|
|
||||||
# Supported Strategies
|
# Bundled Strategies
|
||||||
* Local (with and without Basic Auth)
|
* 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
|
# Default Authentication Callback
|
||||||
A frequent use case within SPA's is opening OAuth login endpoints in a separate window.
|
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.
|
in this package.
|
||||||
|
|
||||||
```dart
|
```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`.
|
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):
|
`angel_client` [exposes this as a Stream](https://github.com/angel-dart/client#authentication):
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
/// Serializes a user to the session.
|
/// Serializes a user to the session.
|
||||||
typedef Future UserSerializer(user);
|
typedef FutureOr UserSerializer<T>(T user);
|
||||||
|
|
||||||
/// Deserializes a user from the session.
|
/// 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 'options.dart';
|
||||||
import 'strategy.dart';
|
import 'strategy.dart';
|
||||||
|
|
||||||
class AngelAuth extends AngelPlugin {
|
/// Handles authentication within an Angel application.
|
||||||
|
class AngelAuth<T> extends AngelPlugin {
|
||||||
Hmac _hs256;
|
Hmac _hs256;
|
||||||
num _jwtLifeSpan;
|
num _jwtLifeSpan;
|
||||||
|
final StreamController<T> _onLogin = new StreamController<T>(),
|
||||||
|
_onLogout = new StreamController<T>();
|
||||||
Math.Random _random = new Math.Random.secure();
|
Math.Random _random = new Math.Random.secure();
|
||||||
final RegExp _rgxBearer = new RegExp(r"^Bearer");
|
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;
|
Hmac get hmac => _hs256;
|
||||||
|
|
||||||
String _randomString(
|
String _randomString(
|
||||||
|
@ -31,9 +60,7 @@ class AngelAuth extends AngelPlugin {
|
||||||
String validChars:
|
String validChars:
|
||||||
"ABCDEFHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_"}) {
|
"ABCDEFHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_"}) {
|
||||||
var chars = <int>[];
|
var chars = <int>[];
|
||||||
|
|
||||||
while (chars.length < length) chars.add(_random.nextInt(validChars.length));
|
while (chars.length < length) chars.add(_random.nextInt(validChars.length));
|
||||||
|
|
||||||
return new String.fromCharCodes(chars);
|
return new String.fromCharCodes(chars);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,6 +80,13 @@ class AngelAuth extends AngelPlugin {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
call(Angel app) async {
|
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);
|
app.container.singleton(this);
|
||||||
if (runtimeType != AngelAuth) app.container.singleton(this, as: AngelAuth);
|
if (runtimeType != AngelAuth) app.container.singleton(this, as: AngelAuth);
|
||||||
|
|
||||||
|
@ -62,6 +96,10 @@ class AngelAuth extends AngelPlugin {
|
||||||
if (reviveTokenEndpoint != null) {
|
if (reviveTokenEndpoint != null) {
|
||||||
app.post(reviveTokenEndpoint, reviveJwt);
|
app.post(reviveTokenEndpoint, reviveJwt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
app.justBeforeStop.add((_) {
|
||||||
|
_onLogin.close();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _apply(RequestContext req, AuthToken token, user) {
|
void _apply(RequestContext req, AuthToken token, user) {
|
||||||
|
@ -70,6 +108,7 @@ class AngelAuth extends AngelPlugin {
|
||||||
..inject(user.runtimeType, req.properties["user"] = user);
|
..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 {
|
decodeJwt(RequestContext req, ResponseContext res) async {
|
||||||
if (req.method == "POST" && req.path == reviveTokenEndpoint) {
|
if (req.method == "POST" && req.path == reviveTokenEndpoint) {
|
||||||
// Shouldn't block invalid JWT if we are reviving it
|
// Shouldn't block invalid JWT if we are reviving it
|
||||||
|
@ -129,6 +168,7 @@ class AngelAuth extends AngelPlugin {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Retrieves a JWT from a request, if any was sent at all.
|
||||||
getJwt(RequestContext req) {
|
getJwt(RequestContext req) {
|
||||||
if (debug) {
|
if (debug) {
|
||||||
print('Attempting to parse JWT');
|
print('Attempting to parse JWT');
|
||||||
|
@ -155,6 +195,7 @@ class AngelAuth extends AngelPlugin {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Attempts to revive an expired (or still alive) JWT.
|
||||||
reviveJwt(RequestContext req, ResponseContext res) async {
|
reviveJwt(RequestContext req, ResponseContext res) async {
|
||||||
try {
|
try {
|
||||||
if (debug) print('Attempting to revive JWT...');
|
if (debug) print('Attempting to revive JWT...');
|
||||||
|
@ -228,10 +269,27 @@ 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]) {
|
authenticate(String type, [AngelAuthOptions options]) {
|
||||||
return (RequestContext req, ResponseContext res) async {
|
return (RequestContext req, ResponseContext res) async {
|
||||||
|
var names = type
|
||||||
|
.split(',')
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.where((String s) => s.isNotEmpty)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
for (int i = 0; i < names.length; i++) {
|
||||||
|
var name = names[i];
|
||||||
|
|
||||||
AuthStrategy strategy =
|
AuthStrategy strategy =
|
||||||
strategies.firstWhere((AuthStrategy x) => x.name == type);
|
strategies.firstWhere((AuthStrategy x) => x.name == name);
|
||||||
var result = await strategy.authenticate(req, res, options);
|
var result = await strategy.authenticate(req, res, options);
|
||||||
if (result == true)
|
if (result == true)
|
||||||
return result;
|
return result;
|
||||||
|
@ -266,30 +324,30 @@ class AngelAuth extends AngelPlugin {
|
||||||
req.headers.value("accept").contains("*/*") ||
|
req.headers.value("accept").contains("*/*") ||
|
||||||
req.headers.value("accept").contains("application/*"))) {
|
req.headers.value("accept").contains("application/*"))) {
|
||||||
var user = await deserializer(await serializer(result));
|
var user = await deserializer(await serializer(result));
|
||||||
|
_onLogin.add(user);
|
||||||
return {"data": user, "token": jwt};
|
return {"data": user, "token": jwt};
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
|
if (i < names.length - 1) continue;
|
||||||
// Check if not redirect
|
// Check if not redirect
|
||||||
if (res.statusCode == 301 ||
|
if (res.statusCode == 301 ||
|
||||||
res.statusCode == 302 ||
|
res.statusCode == 302 ||
|
||||||
res.headers.containsKey(HttpHeaders.LOCATION))
|
res.headers.containsKey(HttpHeaders.LOCATION))
|
||||||
return false;
|
return false;
|
||||||
else
|
else
|
||||||
await authenticationFailure(req, res);
|
throw new AngelHttpException.notAuthenticated();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
Future authenticationFailure(RequestContext req, ResponseContext res) async {
|
|
||||||
throw new AngelHttpException.notAuthenticated();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Log a user in on-demand.
|
/// Log a user in on-demand.
|
||||||
Future login(AuthToken token, RequestContext req, ResponseContext res) async {
|
Future login(AuthToken token, RequestContext req, ResponseContext res) async {
|
||||||
var user = await deserializer(token.userId);
|
var user = await deserializer(token.userId);
|
||||||
_apply(req, token, user);
|
_apply(req, token, user);
|
||||||
|
_onLogin.add(user);
|
||||||
|
|
||||||
if (allowCookie)
|
if (allowCookie)
|
||||||
res.cookies.add(new Cookie('token', token.serialize(_hs256)));
|
res.cookies.add(new Cookie('token', token.serialize(_hs256)));
|
||||||
|
@ -301,11 +359,13 @@ class AngelAuth extends AngelPlugin {
|
||||||
var token = new AuthToken(
|
var token = new AuthToken(
|
||||||
userId: userId, lifeSpan: _jwtLifeSpan, ipAddress: req.ip);
|
userId: userId, lifeSpan: _jwtLifeSpan, ipAddress: req.ip);
|
||||||
_apply(req, token, user);
|
_apply(req, token, user);
|
||||||
|
_onLogin.add(user);
|
||||||
|
|
||||||
if (allowCookie)
|
if (allowCookie)
|
||||||
res.cookies.add(new Cookie('token', token.serialize(_hs256)));
|
res.cookies.add(new Cookie('token', token.serialize(_hs256)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Log an authenticated user out.
|
||||||
logout([AngelAuthOptions options]) {
|
logout([AngelAuthOptions options]) {
|
||||||
return (RequestContext req, ResponseContext res) async {
|
return (RequestContext req, ResponseContext res) async {
|
||||||
for (AuthStrategy strategy in strategies) {
|
for (AuthStrategy strategy in strategies) {
|
||||||
|
@ -320,6 +380,13 @@ class AngelAuth extends AngelPlugin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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");
|
res.cookies.removeWhere((cookie) => cookie.name == "token");
|
||||||
|
|
||||||
if (options != null &&
|
if (options != null &&
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
name: angel_auth
|
name: angel_auth
|
||||||
description: A complete authentication plugin for Angel.
|
description: A complete authentication plugin for Angel.
|
||||||
version: 1.0.4+1
|
version: 1.0.5
|
||||||
author: Tobe O <thosakwe@gmail.com>
|
author: Tobe O <thosakwe@gmail.com>
|
||||||
homepage: https://github.com/angel-dart/angel_auth
|
homepage: https://github.com/angel-dart/angel_auth
|
||||||
environment:
|
environment:
|
||||||
|
|
|
@ -26,11 +26,12 @@ main() {
|
||||||
.service('users')
|
.service('users')
|
||||||
.create({'username': 'jdoe1', 'password': 'password'});
|
.create({'username': 'jdoe1', 'password': 'password'});
|
||||||
|
|
||||||
await app.configure(auth = new AngelAuth());
|
auth = new AngelAuth();
|
||||||
|
|
||||||
auth.serializer = (User user) async => user.id;
|
auth.serializer = (User user) async => user.id;
|
||||||
auth.deserializer = app.service('users').read;
|
auth.deserializer = app.service('users').read;
|
||||||
|
|
||||||
|
await app.configure(auth);
|
||||||
|
|
||||||
auth.strategies.add(new LocalAuthStrategy((username, password) async {
|
auth.strategies.add(new LocalAuthStrategy((username, password) async {
|
||||||
final List<User> users = await app.service('users').index();
|
final List<User> users = await app.service('users').index();
|
||||||
final found = users.firstWhere(
|
final found = users.firstWhere(
|
||||||
|
|
Loading…
Reference in a new issue