Fixed commas
This commit is contained in:
parent
f29fcbf3c9
commit
9fc970e77a
7 changed files with 51 additions and 111 deletions
|
@ -5,9 +5,7 @@
|
|||
<excludeFolder url="file://$MODULE_DIR$/.pub" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/test/packages" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
|
|
10
README.md
10
README.md
|
@ -33,9 +33,15 @@ configureServer(Angel app) async {
|
|||
// If the last strategy throws an authentication failure, then
|
||||
// a `401 Not Authenticated` is thrown.
|
||||
var chainedHandler = auth.authenticate(
|
||||
'basic,facebook',
|
||||
['basic','facebook'],
|
||||
authOptions
|
||||
);
|
||||
|
||||
// Apply angel_auth-specific configuration
|
||||
await app.configure(auth.configureServer);
|
||||
|
||||
// Middleware to decode JWT's...
|
||||
app.use(auth.decodeJwt);
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -60,7 +66,7 @@ configureServer(Angel app) async {
|
|||
// If the last strategy throws an authentication failure, then
|
||||
// a `401 Not Authenticated` is thrown.
|
||||
var chainedHandler = auth.authenticate(
|
||||
'basic,facebook',
|
||||
['basic','facebook'],
|
||||
authOptions
|
||||
);
|
||||
}
|
||||
|
|
2
analysis_options.yaml
Normal file
2
analysis_options.yaml
Normal file
|
@ -0,0 +1,2 @@
|
|||
analyzer:
|
||||
strong-mode: true
|
|
@ -10,7 +10,7 @@ import 'options.dart';
|
|||
import 'strategy.dart';
|
||||
|
||||
/// Handles authentication within an Angel application.
|
||||
class AngelAuth<T> extends AngelPlugin {
|
||||
class AngelAuth<T> {
|
||||
Hmac _hs256;
|
||||
num _jwtLifeSpan;
|
||||
final StreamController<T> _onLogin = new StreamController<T>(),
|
||||
|
@ -27,8 +27,6 @@ class AngelAuth<T> extends AngelPlugin {
|
|||
/// 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.
|
||||
|
@ -69,7 +67,6 @@ class AngelAuth<T> extends AngelPlugin {
|
|||
num jwtLifeSpan,
|
||||
this.allowCookie: true,
|
||||
this.allowTokenInQuery: true,
|
||||
this.debug: false,
|
||||
this.enforceIp: true,
|
||||
this.middlewareName: 'auth',
|
||||
this.reviveTokenEndpoint: "/auth/token"})
|
||||
|
@ -78,8 +75,7 @@ class AngelAuth<T> extends AngelPlugin {
|
|||
_jwtLifeSpan = jwtLifeSpan ?? -1;
|
||||
}
|
||||
|
||||
@override
|
||||
call(Angel app) async {
|
||||
Future configureServer(Angel app) async {
|
||||
if (serializer == null)
|
||||
throw new StateError(
|
||||
'An `AngelAuth` plug-in was called without its `serializer` being set. All authentication will fail.');
|
||||
|
@ -90,14 +86,13 @@ class AngelAuth<T> extends AngelPlugin {
|
|||
app.container.singleton(this);
|
||||
if (runtimeType != AngelAuth) app.container.singleton(this, as: AngelAuth);
|
||||
|
||||
app.before.add(decodeJwt);
|
||||
app.registerMiddleware(middlewareName, requireAuth);
|
||||
|
||||
if (reviveTokenEndpoint != null) {
|
||||
app.post(reviveTokenEndpoint, reviveJwt);
|
||||
}
|
||||
|
||||
app.justBeforeStop.add((_) {
|
||||
app.shutdownHooks.add((_) {
|
||||
_onLogin.close();
|
||||
});
|
||||
}
|
||||
|
@ -109,56 +104,27 @@ class AngelAuth<T> extends AngelPlugin {
|
|||
}
|
||||
|
||||
/// A middleware that decodes a JWT from a request, and injects a corresponding user.
|
||||
decodeJwt(RequestContext req, ResponseContext res) async {
|
||||
Future decodeJwt(RequestContext req, ResponseContext res) async {
|
||||
if (req.method == "POST" && req.path == reviveTokenEndpoint) {
|
||||
// Shouldn't block invalid JWT if we are reviving it
|
||||
if (debug) print('Token revival endpoint accessed.');
|
||||
return await reviveJwt(req, res);
|
||||
}
|
||||
|
||||
if (debug) {
|
||||
print('Enforcing JWT authentication...');
|
||||
}
|
||||
|
||||
String jwt = getJwt(req);
|
||||
|
||||
if (debug) {
|
||||
print('Found JWT: $jwt');
|
||||
}
|
||||
|
||||
if (jwt != null) {
|
||||
var token = new AuthToken.validate(jwt, _hs256);
|
||||
|
||||
if (debug) {
|
||||
print('Decoded auth token: ${token.toJson()}');
|
||||
}
|
||||
|
||||
if (enforceIp) {
|
||||
if (debug) {
|
||||
print('Token IP: ${token.ipAddress}. Current request sent from: ${req
|
||||
.ip}');
|
||||
}
|
||||
|
||||
if (req.ip != null && req.ip != token.ipAddress)
|
||||
throw new AngelHttpException.forbidden(
|
||||
message: "JWT cannot be accessed from this IP address.");
|
||||
}
|
||||
|
||||
if (token.lifeSpan > -1) {
|
||||
if (debug) {
|
||||
print("Making sure this token hasn't already expired...");
|
||||
}
|
||||
|
||||
token.issuedAt.add(new Duration(milliseconds: token.lifeSpan));
|
||||
|
||||
if (!token.issuedAt.isAfter(new DateTime.now()))
|
||||
throw new AngelHttpException.forbidden(message: "Expired JWT.");
|
||||
} else if (debug) {
|
||||
print('This token has an infinite life span.');
|
||||
}
|
||||
|
||||
if (debug) {
|
||||
print('Now deserializing from this userId: ${token.userId}');
|
||||
}
|
||||
|
||||
final user = await deserializer(token.userId);
|
||||
|
@ -169,16 +135,8 @@ class AngelAuth<T> extends AngelPlugin {
|
|||
}
|
||||
|
||||
/// Retrieves a JWT from a request, if any was sent at all.
|
||||
getJwt(RequestContext req) {
|
||||
if (debug) {
|
||||
print('Attempting to parse JWT');
|
||||
}
|
||||
|
||||
String getJwt(RequestContext req) {
|
||||
if (req.headers.value("Authorization") != null) {
|
||||
if (debug) {
|
||||
print('Found Auth header');
|
||||
}
|
||||
|
||||
final authHeader = req.headers.value("Authorization");
|
||||
|
||||
// Allow Basic auth to fall through
|
||||
|
@ -186,7 +144,6 @@ class AngelAuth<T> extends AngelPlugin {
|
|||
return authHeader.replaceAll(_rgxBearer, "").trim();
|
||||
} else if (allowCookie &&
|
||||
req.cookies.any((cookie) => cookie.name == "token")) {
|
||||
if (debug) print('Request has "token" cookie...');
|
||||
return req.cookies.firstWhere((cookie) => cookie.name == "token").value;
|
||||
} else if (allowTokenInQuery && req.query['token'] is String) {
|
||||
return req.query['token'];
|
||||
|
@ -196,43 +153,25 @@ class AngelAuth<T> extends AngelPlugin {
|
|||
}
|
||||
|
||||
/// Attempts to revive an expired (or still alive) JWT.
|
||||
reviveJwt(RequestContext req, ResponseContext res) async {
|
||||
Future<Map<String, dynamic>> reviveJwt(RequestContext req, ResponseContext res) async {
|
||||
try {
|
||||
if (debug) print('Attempting to revive JWT...');
|
||||
|
||||
var jwt = getJwt(req);
|
||||
|
||||
if (jwt == null) {
|
||||
var body = await req.lazyBody();
|
||||
jwt = body['token'];
|
||||
}
|
||||
|
||||
if (debug) print('Found JWT: $jwt');
|
||||
|
||||
if (jwt == null) {
|
||||
throw new AngelHttpException.forbidden(message: "No JWT provided");
|
||||
} else {
|
||||
var token = new AuthToken.validate(jwt, _hs256);
|
||||
|
||||
if (debug) print('Validated and deserialized: $token');
|
||||
|
||||
if (enforceIp) {
|
||||
if (debug)
|
||||
print(
|
||||
'Token IP: ${token.ipAddress}. Current request sent from: ${req
|
||||
.ip}');
|
||||
|
||||
if (req.ip != token.ipAddress)
|
||||
throw new AngelHttpException.forbidden(
|
||||
message: "JWT cannot be accessed from this IP address.");
|
||||
}
|
||||
|
||||
if (token.lifeSpan > -1) {
|
||||
if (debug) {
|
||||
print('Checking if token has expired... Life span is ${token
|
||||
.lifeSpan}');
|
||||
}
|
||||
|
||||
token.issuedAt.add(new Duration(milliseconds: token.lifeSpan));
|
||||
|
||||
if (!token.issuedAt.isAfter(new DateTime.now())) {
|
||||
|
@ -240,15 +179,7 @@ class AngelAuth<T> extends AngelPlugin {
|
|||
'Token has indeed expired! Resetting assignment date to current timestamp...');
|
||||
// Extend its lifespan by changing iat
|
||||
token.issuedAt = new DateTime.now();
|
||||
} else if (debug) {
|
||||
print('Token has not expired yet.');
|
||||
}
|
||||
} else if (debug) {
|
||||
print('This token never expires, so it is still valid.');
|
||||
}
|
||||
|
||||
if (debug) {
|
||||
print('Final, valid token: ${token.toJson()}');
|
||||
}
|
||||
|
||||
if (allowCookie)
|
||||
|
@ -257,13 +188,7 @@ class AngelAuth<T> extends AngelPlugin {
|
|||
final data = await deserializer(token.userId);
|
||||
return {'data': data, 'token': token.serialize(_hs256)};
|
||||
}
|
||||
} catch (e, st) {
|
||||
if (debug) {
|
||||
print('An error occurred while reviving this token.');
|
||||
print(e);
|
||||
print(st);
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
if (e is AngelHttpException) rethrow;
|
||||
throw new AngelHttpException.badRequest(message: "Malformed JWT");
|
||||
}
|
||||
|
@ -271,19 +196,25 @@ class AngelAuth<T> extends AngelPlugin {
|
|||
|
||||
/// Attempts to authenticate a user using one or more strategies.
|
||||
///
|
||||
/// [type] is a comma-separated list of strategy names to try.
|
||||
/// [type] is a strategy name to try, or a `List` of such.
|
||||
///
|
||||
/// 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]) {
|
||||
RequestHandler authenticate(type, [AngelAuthOptions options]) {
|
||||
return (RequestContext req, ResponseContext res) async {
|
||||
var names = type
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.where((String s) => s.isNotEmpty)
|
||||
.toList();
|
||||
List<String> names = [];
|
||||
var arr = type is Iterable ? type.toList() : [type];
|
||||
|
||||
for (var t in arr) {
|
||||
var n = t
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.where((String s) => s.isNotEmpty)
|
||||
.toList();
|
||||
names.addAll(n);
|
||||
}
|
||||
|
||||
for (int i = 0; i < names.length; i++) {
|
||||
var name = names[i];
|
||||
|
@ -368,14 +299,14 @@ class AngelAuth<T> extends AngelPlugin {
|
|||
}
|
||||
|
||||
/// Log an authenticated user out.
|
||||
logout([AngelAuthOptions options]) {
|
||||
RequestMiddleware logout([AngelAuthOptions options]) {
|
||||
return (RequestContext req, ResponseContext res) async {
|
||||
for (AuthStrategy strategy in strategies) {
|
||||
if (!(await strategy.canLogout(req, res))) {
|
||||
if (options != null &&
|
||||
options.failureRedirect != null &&
|
||||
options.failureRedirect.isNotEmpty) {
|
||||
return res.redirect(options.failureRedirect);
|
||||
res.redirect(options.failureRedirect);
|
||||
}
|
||||
|
||||
return false;
|
||||
|
@ -394,7 +325,7 @@ class AngelAuth<T> extends AngelPlugin {
|
|||
if (options != null &&
|
||||
options.successRedirect != null &&
|
||||
options.successRedirect.isNotEmpty) {
|
||||
return res.redirect(options.successRedirect);
|
||||
res.redirect(options.successRedirect);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
name: angel_auth
|
||||
description: A complete authentication plugin for Angel.
|
||||
version: 1.0.5
|
||||
version: 1.1.0-alpha
|
||||
author: Tobe O <thosakwe@gmail.com>
|
||||
homepage: https://github.com/angel-dart/angel_auth
|
||||
environment:
|
||||
sdk: ">=1.19.0"
|
||||
dependencies:
|
||||
angel_framework: ^1.0.0-dev
|
||||
angel_framework: ^1.1.0-alpha
|
||||
crypto: ^2.0.0
|
||||
dev_dependencies:
|
||||
http: ^0.11.0
|
||||
|
|
|
@ -30,7 +30,8 @@ main() {
|
|||
auth.serializer = (User user) async => user.id;
|
||||
auth.deserializer = app.service('users').read;
|
||||
|
||||
await app.configure(auth);
|
||||
await app.configure(auth.configureServer);
|
||||
app.use(auth.decodeJwt);
|
||||
|
||||
auth.strategies.add(new LocalAuthStrategy((username, password) async {
|
||||
final List<User> users = await app.service('users').index();
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:angel_framework/angel_framework.dart';
|
||||
|
@ -5,25 +6,26 @@ import 'package:angel_auth/angel_auth.dart';
|
|||
import 'package:http/http.dart' as http;
|
||||
import 'package:test/test.dart';
|
||||
|
||||
final AngelAuth Auth = new AngelAuth();
|
||||
final AngelAuth auth = new AngelAuth();
|
||||
Map headers = {HttpHeaders.ACCEPT: ContentType.JSON.mimeType};
|
||||
AngelAuthOptions localOpts = new AngelAuthOptions(
|
||||
failureRedirect: '/failure', successRedirect: '/success');
|
||||
Map sampleUser = {'hello': 'world'};
|
||||
|
||||
verifier(username, password) async {
|
||||
Future verifier(String username, String password) async {
|
||||
if (username == 'username' && password == 'password') {
|
||||
return sampleUser;
|
||||
} else
|
||||
return false;
|
||||
}
|
||||
|
||||
wireAuth(Angel app) async {
|
||||
Auth.serializer = (user) async => 1337;
|
||||
Auth.deserializer = (id) async => sampleUser;
|
||||
Future wireAuth(Angel app) async {
|
||||
auth.serializer = (user) async => 1337;
|
||||
auth.deserializer = (id) async => sampleUser;
|
||||
|
||||
Auth.strategies.add(new LocalAuthStrategy(verifier));
|
||||
await app.configure(Auth);
|
||||
auth.strategies.add(new LocalAuthStrategy(verifier));
|
||||
await app.configure(auth.configureServer);
|
||||
app.use(auth.decodeJwt);
|
||||
}
|
||||
|
||||
main() async {
|
||||
|
@ -34,11 +36,11 @@ main() async {
|
|||
|
||||
setUp(() async {
|
||||
client = new http.Client();
|
||||
app = new Angel(debug: true);
|
||||
app = new Angel();
|
||||
await app.configure(wireAuth);
|
||||
app.get('/hello', 'Woo auth', middleware: [Auth.authenticate('local')]);
|
||||
app.get('/hello', 'Woo auth', middleware: [auth.authenticate('local')]);
|
||||
app.post('/login', 'This should not be shown',
|
||||
middleware: [Auth.authenticate('local', localOpts)]);
|
||||
middleware: [auth.authenticate('local', localOpts)]);
|
||||
app.get('/success', "yep", middleware: ['auth']);
|
||||
app.get('/failure', "nope");
|
||||
|
||||
|
@ -95,8 +97,8 @@ main() async {
|
|||
});
|
||||
|
||||
test('force basic', () async {
|
||||
Auth.strategies.clear();
|
||||
Auth.strategies
|
||||
auth.strategies.clear();
|
||||
auth.strategies
|
||||
.add(new LocalAuthStrategy(verifier, forceBasic: true, realm: 'test'));
|
||||
var response = await client.get("$url/hello", headers: headers);
|
||||
print(response.headers);
|
||||
|
|
Loading…
Reference in a new issue