Add 'packages/auth_oauth2/' from commit '97bd61e7ee6736c8a871d6cc45a5203e07bb03e6'
git-subtree-dir: packages/auth_oauth2 git-subtree-mainline:a584b8ce02
git-subtree-split:97bd61e7ee
This commit is contained in:
commit
c77d25f6d0
12 changed files with 512 additions and 0 deletions
75
packages/auth_oauth2/.gitignore
vendored
Normal file
75
packages/auth_oauth2/.gitignore
vendored
Normal file
|
@ -0,0 +1,75 @@
|
|||
# See https://www.dartlang.org/tools/private-files.html
|
||||
|
||||
# Files and directories created by pub
|
||||
.buildlog
|
||||
.packages
|
||||
.project
|
||||
.pub/
|
||||
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
|
||||
|
||||
# Directory created by dartdoc
|
||||
doc/api/
|
||||
|
||||
# Don't commit pubspec lock file
|
||||
# (Library packages only! Remove pattern if developing an application package)
|
||||
pubspec.lock
|
||||
|
||||
log.txt
|
||||
### 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
|
||||
|
||||
# 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
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
.dart_tool
|
16
packages/auth_oauth2/.idea/auth_oauth2.iml
Normal file
16
packages/auth_oauth2/.idea/auth_oauth2.iml
Normal file
|
@ -0,0 +1,16 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.pub" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||
</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
packages/auth_oauth2/.idea/modules.xml
Normal file
8
packages/auth_oauth2/.idea/modules.xml
Normal 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.iml" filepath="$PROJECT_DIR$/.idea/auth_oauth2.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
|
@ -0,0 +1,7 @@
|
|||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Github Auth Server" type="DartCommandLineRunConfigurationType" factoryName="Dart Command Line Application" singleton="true">
|
||||
<option name="filePath" value="$PROJECT_DIR$/example/main.dart" />
|
||||
<option name="workingDirectory" value="$PROJECT_DIR$" />
|
||||
<method />
|
||||
</configuration>
|
||||
</component>
|
6
packages/auth_oauth2/.idea/vcs.xml
Normal file
6
packages/auth_oauth2/.idea/vcs.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
13
packages/auth_oauth2/CHANGELOG.md
Normal file
13
packages/auth_oauth2/CHANGELOG.md
Normal file
|
@ -0,0 +1,13 @@
|
|||
# 2.1.0
|
||||
* Angel 2 + Dart 2 update
|
||||
* Support for handling errors + rejections.
|
||||
* Use `ExternalAuthOptions`.
|
||||
|
||||
# 2.0.0+1
|
||||
* Meta update to improve Pub score.
|
||||
|
||||
# 2.0.0
|
||||
* Angel 2 + Dart 2 updates.
|
||||
|
||||
# 1.0.2
|
||||
Added `getParameters` to `AngelOAuth2Options`.
|
21
packages/auth_oauth2/LICENSE
Normal file
21
packages/auth_oauth2/LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2017 Tobe O
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
133
packages/auth_oauth2/README.md
Normal file
133
packages/auth_oauth2/README.md
Normal file
|
@ -0,0 +1,133 @@
|
|||
# auth_oauth2
|
||||
|
||||
[![Pub](https://img.shields.io/pub/v/angel_auth_oauth2.svg)](https://pub.dartlang.org/packages/angel_auth_oauth2)
|
||||
|
||||
`package:angel_auth` strategy for OAuth2 login, i.e. Facebook or Github.
|
||||
|
||||
# Usage
|
||||
First, create an options object:
|
||||
|
||||
```dart
|
||||
configureServer(Angel app) async {
|
||||
// Load from a Map, i.e. app config:
|
||||
var opts = ExternalAuthOptions.fromMap(app.configuration['auth0'] as Map);
|
||||
|
||||
// Create in-place:
|
||||
var opts = ExternalAuthOptions(
|
||||
clientId: '<client-id>',
|
||||
clientSecret: '<client-secret>',
|
||||
redirectUri: Uri.parse('<callback>'));
|
||||
}
|
||||
```
|
||||
|
||||
After getting authenticated against the remote server, we need to be able to identify
|
||||
users within our own application.
|
||||
|
||||
```dart
|
||||
typedef FutureOr<User> OAuth2Verifier(oauth2.Client, RequestContext, ResponseContext);
|
||||
|
||||
/// You might use a pure function to create a verifier that queries a
|
||||
/// given service.
|
||||
OAuth2Verifier oauth2verifier(Service<User> userService) {
|
||||
return (client) async {
|
||||
var response = await client.get('https://api.github.com/user');
|
||||
var ghUser = json.decode(response.body);
|
||||
var id = ghUser['id'] as int;
|
||||
|
||||
var matchingUsers = await mappedUserService.index({
|
||||
'query': {'github_id': id}
|
||||
});
|
||||
|
||||
if (matchingUsers.isNotEmpty) {
|
||||
// Return the corresponding user, if it exists.
|
||||
return matchingUsers.first;
|
||||
} else {
|
||||
// Otherwise,create a user
|
||||
return await mappedUserService.create(User(githubId: id));
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Now, initialize an `OAuth2Strategy`, using the options and verifier.
|
||||
You'll also need to provide a name for this instance of the strategy.
|
||||
Consider using the name of the remote authentication provider (ex. `facebook`).
|
||||
|
||||
```dart
|
||||
configureServer(Angel app) {
|
||||
auth.strategies['github'] = OAuth2Strategy(
|
||||
options,
|
||||
authorizationEndpoint,
|
||||
tokenEndpoint,
|
||||
yourVerifier,
|
||||
|
||||
// This function is called when an error occurs, or the user REJECTS the request.
|
||||
(e, req, res) async {
|
||||
res.write('Ooops: $e');
|
||||
await res.close();
|
||||
},
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Lastly, connect it to an `AngelAuth` instance, and wire it up to an `Angel` server.
|
||||
Set up two routes:
|
||||
1. Redirect users to the external provider
|
||||
2. Acts as a callback and handles an access code
|
||||
|
||||
In the case of the callback route, you may want to display an HTML page that closes
|
||||
a popup window. In this case, use `confirmPopupAuthentication`, which is bundled with
|
||||
`package:angel_auth`, as a `callback` function:
|
||||
|
||||
```dart
|
||||
configureServer(Angel app) async {
|
||||
// ...
|
||||
var auth = AngelAuth<User>();
|
||||
auth.strategies['github'] = oauth2Strategy;
|
||||
|
||||
// Redirect
|
||||
app.get('/auth/github', auth.authenticate('github'));
|
||||
|
||||
// Callback
|
||||
app.get('/auth/github/callback', auth.authenticate(
|
||||
'github',
|
||||
AngelAuthOptions(callback: confirmPopupAuthentication())
|
||||
));
|
||||
|
||||
// Connect the plug-in!!!
|
||||
await app.configure(auth);
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Scope Delimiter
|
||||
This package should work out-of-the-box for most OAuth2 providers, such as Github or Dropbox.
|
||||
However, if your OAuth2 scopes are separated by a delimiter other than the default (`' '`),
|
||||
you can add it in the `OAuth2Strategy` constructor:
|
||||
|
||||
```dart
|
||||
configureServer(Angel app) async {
|
||||
OAuth2Strategy(..., delimiter: ' ');
|
||||
}
|
||||
```
|
||||
|
||||
## Handling non-JSON responses
|
||||
Many OAuth2 providers do not follow the specification, and do not return
|
||||
`application/json` responses.
|
||||
|
||||
You can add a `getParameters` callback to parse the contents of any arbitrary
|
||||
response:
|
||||
|
||||
```dart
|
||||
OAuth2Strategy(
|
||||
// ...
|
||||
getParameters: (contentType, body) {
|
||||
if (contentType.type == 'application') {
|
||||
if (contentType.subtype == 'x-www-form-urlencoded')
|
||||
return Uri.splitQueryString(body);
|
||||
else if (contentType.subtype == 'json') return JSON.decode(body);
|
||||
}
|
||||
|
||||
throw FormatException('Invalid content-type $contentType; expected application/x-www-form-urlencoded or application/json.');
|
||||
}
|
||||
);
|
||||
```
|
4
packages/auth_oauth2/analysis_options.yaml
Normal file
4
packages/auth_oauth2/analysis_options.yaml
Normal file
|
@ -0,0 +1,4 @@
|
|||
include: package:pedantic/analysis_options.yaml
|
||||
analyzer:
|
||||
strong-mode:
|
||||
implicit-casts: false
|
127
packages/auth_oauth2/example/main.dart
Normal file
127
packages/auth_oauth2/example/main.dart
Normal file
|
@ -0,0 +1,127 @@
|
|||
import 'dart:convert';
|
||||
import 'package:angel_auth/angel_auth.dart';
|
||||
import 'package:angel_framework/angel_framework.dart';
|
||||
import 'package:angel_framework/http.dart';
|
||||
import 'package:angel_auth_oauth2/angel_auth_oauth2.dart';
|
||||
import 'package:http_parser/http_parser.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
var authorizationEndpoint =
|
||||
Uri.parse('http://github.com/login/oauth/authorize');
|
||||
|
||||
var tokenEndpoint = Uri.parse('https://github.com/login/oauth/access_token');
|
||||
|
||||
var options = ExternalAuthOptions(
|
||||
clientId: '6caeaf5d4c04936ec34f',
|
||||
clientSecret: '178360518cf9de4802e2346a4b6ebec525dc4427',
|
||||
redirectUri: Uri.parse('http://localhost:3000/auth/github/callback'),
|
||||
);
|
||||
|
||||
/// Github doesn't properly follow the OAuth2 spec, so here's logic to parse their response.
|
||||
Map<String, dynamic> parseParamsFromGithub(MediaType contentType, String body) {
|
||||
if (contentType.type == 'application') {
|
||||
if (contentType.subtype == 'x-www-form-urlencoded')
|
||||
return Uri.splitQueryString(body);
|
||||
else if (contentType.subtype == 'json')
|
||||
return (json.decode(body) as Map).cast<String, String>();
|
||||
}
|
||||
|
||||
throw FormatException(
|
||||
'Invalid content-type $contentType; expected application/x-www-form-urlencoded or application/json.');
|
||||
}
|
||||
|
||||
main() async {
|
||||
// Create the server instance.
|
||||
var app = Angel();
|
||||
var http = AngelHttp(app);
|
||||
app.logger = Logger('angel')
|
||||
..onRecord.listen((rec) {
|
||||
print(rec);
|
||||
if (rec.error != null) print(rec.error);
|
||||
if (rec.stackTrace != null) print(rec.stackTrace);
|
||||
});
|
||||
|
||||
// Create a service that stores user data.
|
||||
var userService = app.use('/users', MapService()).inner;
|
||||
var mappedUserService = userService.map(User.parse, User.serialize);
|
||||
|
||||
// Set up the authenticator plugin.
|
||||
var auth =
|
||||
AngelAuth<User>(jwtKey: 'oauth2 example secret', allowCookie: false);
|
||||
auth.serializer = (user) async => user.id;
|
||||
auth.deserializer = (id) => mappedUserService.read(id.toString());
|
||||
app.fallback(auth.decodeJwt);
|
||||
|
||||
/// Create an instance of the strategy class.
|
||||
auth.strategies['github'] = OAuth2Strategy(
|
||||
options,
|
||||
authorizationEndpoint,
|
||||
tokenEndpoint,
|
||||
|
||||
// This function is called when the user ACCEPTS the request to sign in with Github.
|
||||
(client, req, res) async {
|
||||
var response = await client.get('https://api.github.com/user');
|
||||
var ghUser = json.decode(response.body);
|
||||
var id = ghUser['id'] as int;
|
||||
|
||||
var matchingUsers = await mappedUserService.index({
|
||||
'query': {'github_id': id}
|
||||
});
|
||||
|
||||
if (matchingUsers.isNotEmpty) {
|
||||
// Return the corresponding user, if it exists.
|
||||
return matchingUsers.first;
|
||||
} else {
|
||||
// Otherwise,create a user
|
||||
return await mappedUserService.create(User(githubId: id));
|
||||
}
|
||||
},
|
||||
|
||||
// This function is called when an error occurs, or the user REJECTS the request.
|
||||
(e, req, res) async {
|
||||
res.write('Ooops: $e');
|
||||
await res.close();
|
||||
},
|
||||
|
||||
// We have to pass this parser function when working with Github.
|
||||
getParameters: parseParamsFromGithub,
|
||||
);
|
||||
|
||||
// Mount some routes
|
||||
app.get('/auth/github', auth.authenticate('github'));
|
||||
app.get(
|
||||
'/auth/github/callback',
|
||||
auth.authenticate('github',
|
||||
AngelAuthOptions(callback: (req, res, jwt) async {
|
||||
// In real-life, you might include a pop-up callback script.
|
||||
//
|
||||
// Use `confirmPopupAuthentication`, which is bundled with
|
||||
// `package:angel_auth`.
|
||||
var user = req.container.make<User>();
|
||||
res.write('Your user info: ${user.toJson()}\n\n');
|
||||
res.write('Your JWT: $jwt');
|
||||
await res.close();
|
||||
})));
|
||||
|
||||
// Start listening.
|
||||
await http.startServer('127.0.0.1', 3000);
|
||||
print('Listening on ${http.uri}');
|
||||
print('View user listing: ${http.uri}/users');
|
||||
print('Sign in via Github: ${http.uri}/auth/github');
|
||||
}
|
||||
|
||||
class User extends Model {
|
||||
@override
|
||||
String id;
|
||||
|
||||
int githubId;
|
||||
|
||||
User({this.id, this.githubId});
|
||||
|
||||
static User parse(Map map) =>
|
||||
User(id: map['id'] as String, githubId: map['github_id'] as int);
|
||||
|
||||
static Map<String, dynamic> serialize(User user) => user.toJson();
|
||||
|
||||
Map<String, dynamic> toJson() => {'id': id, 'github_id': githubId};
|
||||
}
|
87
packages/auth_oauth2/lib/angel_auth_oauth2.dart
Normal file
87
packages/auth_oauth2/lib/angel_auth_oauth2.dart
Normal file
|
@ -0,0 +1,87 @@
|
|||
library angel_auth_oauth2;
|
||||
|
||||
import 'dart:async';
|
||||
import 'package:angel_auth/angel_auth.dart';
|
||||
import 'package:angel_framework/angel_framework.dart';
|
||||
import 'package:http_parser/http_parser.dart';
|
||||
import 'package:oauth2/oauth2.dart' as oauth2;
|
||||
|
||||
/// An Angel [AuthStrategy] that signs users in via a third-party service that speaks OAuth 2.0.
|
||||
class OAuth2Strategy<User> implements AuthStrategy<User> {
|
||||
/// A callback that uses the third-party service to authenticate a [User].
|
||||
///
|
||||
/// As always, return `null` if authentication fails.
|
||||
final FutureOr<User> Function(oauth2.Client, RequestContext, ResponseContext)
|
||||
verifier;
|
||||
|
||||
/// A callback that is triggered when an OAuth2 error occurs (i.e. the user declines to login);
|
||||
final FutureOr<dynamic> Function(
|
||||
oauth2.AuthorizationException, RequestContext, ResponseContext) onError;
|
||||
|
||||
/// The options defining how to connect to the third-party.
|
||||
final ExternalAuthOptions options;
|
||||
|
||||
/// The URL to query to receive an authentication code.
|
||||
final Uri authorizationEndpoint;
|
||||
|
||||
/// The URL to query to exchange an authentication code for a token.
|
||||
final Uri tokenEndpoint;
|
||||
|
||||
/// An optional callback used to parse the response from a server who does not follow the OAuth 2.0 spec.
|
||||
final Map<String, dynamic> Function(MediaType, String) getParameters;
|
||||
|
||||
/// An optional delimiter used to send requests to server who does not follow the OAuth 2.0 spec.
|
||||
final String delimiter;
|
||||
|
||||
Uri _redirect;
|
||||
|
||||
OAuth2Strategy(this.options, this.authorizationEndpoint, this.tokenEndpoint,
|
||||
this.verifier, this.onError,
|
||||
{this.getParameters, this.delimiter = ' '});
|
||||
|
||||
oauth2.AuthorizationCodeGrant _createGrant() =>
|
||||
new oauth2.AuthorizationCodeGrant(options.clientId, authorizationEndpoint,
|
||||
tokenEndpoint,
|
||||
secret: options.clientSecret,
|
||||
delimiter: delimiter,
|
||||
getParameters: getParameters);
|
||||
|
||||
@override
|
||||
FutureOr<User> authenticate(RequestContext req, ResponseContext res,
|
||||
[AngelAuthOptions<User> options]) async {
|
||||
if (options != null) {
|
||||
var result = await authenticateCallback(req, res, options);
|
||||
if (result is User)
|
||||
return result;
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
if (_redirect == null) {
|
||||
var grant = _createGrant();
|
||||
_redirect = grant.getAuthorizationUrl(
|
||||
this.options.redirectUri,
|
||||
scopes: this.options.scopes,
|
||||
);
|
||||
}
|
||||
|
||||
res.redirect(_redirect);
|
||||
return null;
|
||||
}
|
||||
|
||||
/// The endpoint that is invoked by the third-party after successful authentication.
|
||||
Future<dynamic> authenticateCallback(RequestContext req, ResponseContext res,
|
||||
[AngelAuthOptions options]) async {
|
||||
var grant = _createGrant();
|
||||
grant.getAuthorizationUrl(this.options.redirectUri,
|
||||
scopes: this.options.scopes);
|
||||
|
||||
try {
|
||||
var client =
|
||||
await grant.handleAuthorizationResponse(req.uri.queryParameters);
|
||||
return await verifier(client, req, res);
|
||||
} on oauth2.AuthorizationException catch (e) {
|
||||
return await onError(e, req, res);
|
||||
}
|
||||
}
|
||||
}
|
15
packages/auth_oauth2/pubspec.yaml
Normal file
15
packages/auth_oauth2/pubspec.yaml
Normal file
|
@ -0,0 +1,15 @@
|
|||
name: angel_auth_oauth2
|
||||
description: angel_auth strategy for OAuth2 login, i.e. Facebook, Github, etc.
|
||||
version: 2.1.0
|
||||
author: Tobe O <thosakwe@gmail.com>
|
||||
environment:
|
||||
sdk: ">=2.0.0-dev <3.0.0"
|
||||
homepage: https://github.com/angel-dart/auth_oauth2.git
|
||||
dependencies:
|
||||
angel_auth: ^2.0.0
|
||||
angel_framework: ^2.0.0-alpha
|
||||
http_parser: ^3.0.0
|
||||
oauth2: ^1.0.0
|
||||
dev_dependencies:
|
||||
logging: ^0.11.0
|
||||
pedantic: ^1.0.0
|
Loading…
Reference in a new issue