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:
Tobe O 2020-02-15 18:28:54 -05:00
commit c77d25f6d0
12 changed files with 512 additions and 0 deletions

75
packages/auth_oauth2/.gitignore vendored Normal file
View 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

View 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>

View 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>

View file

@ -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>

View 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>

View 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`.

View 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.

View 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.');
}
);
```

View file

@ -0,0 +1,4 @@
include: package:pedantic/analysis_options.yaml
analyzer:
strong-mode:
implicit-casts: false

View 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};
}

View 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);
}
}
}

View 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