Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: adding JWT signing to auth tokens #12

Merged
merged 4 commits into from Aug 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
25 changes: 25 additions & 0 deletions api/lib/middlewares/authentication_validator.dart
@@ -0,0 +1,25 @@
import 'package:authentication_repository/authentication_repository.dart';
import 'package:dart_frog/dart_frog.dart';
import 'package:dart_frog_auth/dart_frog_auth.dart';
import 'package:hub_domain/hub_domain.dart';
import 'package:user_repository/user_repository.dart';
import 'package:very_good_hub_api/models/models.dart';

/// Middleware that checks if the request authentication is valid.
///
/// And sets the session in the request context if so.
Middleware authenticationValidator() => bearerAuthentication<ApiSession>(
authenticator: (context, token) async {
final authenticationReposity = context.read<AuthenticationRepository>();
final session = Session.fromJson(authenticationReposity.verify(token));

final userRepository = context.read<UserRepository>();
final user = await userRepository.findUserById(session.userId);

if (user != null) {
return ApiSession(user: user, session: session);
}

return null;
},
);
1 change: 1 addition & 0 deletions api/lib/middlewares/middlewares.dart
@@ -1 +1,2 @@
export 'authentication_validator.dart';
export 'cors_middleware.dart';
32 changes: 31 additions & 1 deletion api/main.dart
@@ -1,18 +1,48 @@
import 'dart:io';

import 'package:authentication_repository/authentication_repository.dart';
import 'package:dart_frog/dart_frog.dart';
import 'package:db_client/db_client.dart';

late final DbClient _dbClient;
late final AuthenticationRepository _authenticationRepository;

Future<void> init(InternetAddress ip, int port) async {
_dbClient = DbClient();
_authenticationRepository = AuthenticationRepository(
secret: _secret,
issuer: _issuer,
);
}

Future<HttpServer> run(Handler handler, InternetAddress ip, int port) async {
return serve(
handler.use(provider<DbClient>((_) => _dbClient)),
handler
.use(
provider<DbClient>((_) => _dbClient),
)
.use(
provider<AuthenticationRepository>((_) => _authenticationRepository),
),
ip,
port,
);
}

String get _secret {
final value = Platform.environment['SECRET'];
if (value == null) {
stdout.writeln('No secret provided, running with development settings');
return 'two is not one, which is not three, but is a number';
}
return value;
}

String get _issuer {
final value = Platform.environment['ISSUER'];
if (value == null) {
stdout.writeln('No issuer provided, running with development settings');
return 'https://localhost:8080';
}
return value;
}
7 changes: 7 additions & 0 deletions api/packages/authentication_repository/.gitignore
@@ -0,0 +1,7 @@
# See https://www.dartlang.org/guides/libraries/private-files

# Files and directories created by pub
.dart_tool/
.packages
build/
pubspec.lock
69 changes: 69 additions & 0 deletions api/packages/authentication_repository/README.md
@@ -0,0 +1,69 @@
# Authentication Repository

[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link]
[![Powered by Mason](https://img.shields.io/endpoint?url=https%3A%2F%2Ftinyurl.com%2Fmason-badge)](https://github.com/felangel/mason)
[![License: MIT][license_badge]][license_link]

API's authentication repository

## Installation 💻

**❗ In order to start using Authentication Repository you must have the [Dart SDK][dart_install_link] installed on your machine.**

Add `authentication_repository` to your `pubspec.yaml`:

```yaml
dependencies:
authentication_repository:
```

Install it:

```sh
dart pub get
```

---

## Continuous Integration 🤖

Authentication Repository comes with a built-in [GitHub Actions workflow][github_actions_link] powered by [Very Good Workflows][very_good_workflows_link] but you can also add your preferred CI/CD solution.

Out of the box, on each pull request and push, the CI `formats`, `lints`, and `tests` the code. This ensures the code remains consistent and behaves correctly as you add functionality or make changes. The project uses [Very Good Analysis][very_good_analysis_link] for a strict set of analysis options used by our team. Code coverage is enforced using the [Very Good Workflows][very_good_coverage_link].

---

## Running Tests 🧪

To run all unit tests:

```sh
dart pub global activate coverage 1.2.0
dart test --coverage=coverage
dart pub global run coverage:format_coverage --lcov --in=coverage --out=coverage/lcov.info
```

To view the generated coverage report you can use [lcov](https://github.com/linux-test-project/lcov).

```sh
# Generate Coverage Report
genhtml coverage/lcov.info -o coverage/

# Open Coverage Report
open coverage/index.html
```

[dart_install_link]: https://dart.dev/get-dart
[github_actions_link]: https://docs.github.com/en/actions/learn-github-actions
[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg
[license_link]: https://opensource.org/licenses/MIT
[logo_black]: https://raw.githubusercontent.com/VGVentures/very_good_brand/main/styles/README/vgv_logo_black.png#gh-light-mode-only
[logo_white]: https://raw.githubusercontent.com/VGVentures/very_good_brand/main/styles/README/vgv_logo_white.png#gh-dark-mode-only
[mason_link]: https://github.com/felangel/mason
[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg
[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis
[very_good_coverage_link]: https://github.com/marketplace/actions/very-good-coverage
[very_good_ventures_link]: https://verygood.ventures
[very_good_ventures_link_light]: https://verygood.ventures#gh-light-mode-only
[very_good_ventures_link_dark]: https://verygood.ventures#gh-dark-mode-only
[very_good_workflows_link]: https://github.com/VeryGoodOpenSource/very_good_workflows
@@ -0,0 +1 @@
include: package:very_good_analysis/analysis_options.5.0.0.yaml
20 changes: 20 additions & 0 deletions api/packages/authentication_repository/coverage_badge.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
@@ -0,0 +1,4 @@
/// API's authentication repository
library authentication_repository;

export 'src/authentication_repository.dart';
@@ -0,0 +1,64 @@
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';

/// {@template authentication_failure}
/// Exception throw when verification fails.
/// {@endtemplate}
class AuthenticationFailure implements Exception {
/// {@macro authentication_failure}
const AuthenticationFailure(this.message, this.stackTrace);

/// The reason verification failed.
final String message;

/// The stack trace of the exception.
final StackTrace? stackTrace;

@override
String toString() => 'AuthenticationFailure(message: $message)';
}

/// Function signature that builds a JWT.
typedef JWTBuilder = JWT Function(dynamic payload, {String? issuer});

/// Function signature that verifies a JWT.
typedef JWTVerifier = JWT Function(String token, JWTKey key);

/// {@template authentication_repository}
/// API's authentication repository.
///
/// Responsible for signing and validating user tokens.
///
/// {@endtemplate}
class AuthenticationRepository {
/// {@macro authentication_repository}
const AuthenticationRepository({
required String secret,
required String issuer,
JWTBuilder jwtBuilder = JWT.new,
JWTVerifier jwtVerifier = JWT.verify,
}) : _secret = secret,
_issuer = issuer,
_jwtBuilder = jwtBuilder,
_jwtVerifier = jwtVerifier;

final String _secret;
final String _issuer;
final JWTBuilder _jwtBuilder;
final JWTVerifier _jwtVerifier;

/// Signs a payload into a token.
String sign(Map<String, dynamic> payload) {
final jwt = _jwtBuilder(payload, issuer: _issuer);
return jwt.sign(SecretKey(_secret));
}

/// Verifies a token and returns the payload.
Map<String, dynamic> verify(String token) {
try {
final jwt = _jwtVerifier(token, SecretKey(_secret));
return jwt.payload as Map<String, dynamic>;
} catch (e, s) {
throw AuthenticationFailure(e.toString(), s);
}
}
}
14 changes: 14 additions & 0 deletions api/packages/authentication_repository/pubspec.yaml
@@ -0,0 +1,14 @@
name: authentication_repository
description: API's authentication repository
version: 0.1.0+1
publish_to: none

environment:
sdk: ">=3.0.0 <4.0.0"

dev_dependencies:
mocktail: ^0.3.0
test: ^1.19.2
very_good_analysis: ^5.0.0
dependencies:
dart_jsonwebtoken: ^2.9.1
@@ -0,0 +1,17 @@
// ignore_for_file: prefer_const_constructors
import 'package:authentication_repository/authentication_repository.dart';
import 'package:test/test.dart';

void main() {
group('AuthenticationRepository', () {
test('can be instantiated', () {
expect(
AuthenticationRepository(
secret: 'two is not equals to four, neither one',
issuer: 'https://the-issuer.com',
),
isNotNull,
);
});
});
}
2 changes: 2 additions & 0 deletions api/pubspec.yaml
Expand Up @@ -7,6 +7,8 @@ environment:
sdk: ">=3.0.0 <4.0.0"

dependencies:
authentication_repository:
path: packages/authentication_repository
dart_frog: ^1.0.0
dart_frog_auth: ^1.1.0
db_client:
Expand Down
10 changes: 9 additions & 1 deletion api/routes/auth/sign_in.dart
@@ -1,5 +1,6 @@
import 'dart:io';

import 'package:authentication_repository/authentication_repository.dart';
import 'package:dart_frog/dart_frog.dart';
import 'package:session_repository/session_repository.dart';
import 'package:user_repository/user_repository.dart';
Expand Down Expand Up @@ -31,7 +32,14 @@ Future<Response> _onPost(RequestContext context) async {

if (user != null) {
final session = await sessionRepository.createSession(user.id);
return Response.json(body: session.toJson());

final authenticationRepository = context.read<AuthenticationRepository>();
final signedSession = authenticationRepository.sign(session.toJson());
return Response.json(
body: {
'token': signedSession,
},
);
} else {
return Response(statusCode: HttpStatus.unauthorized);
}
Expand Down
22 changes: 1 addition & 21 deletions api/routes/hub/_middleware.dart
@@ -1,32 +1,12 @@
import 'package:dart_frog/dart_frog.dart';
import 'package:dart_frog_auth/dart_frog_auth.dart';
import 'package:db_client/db_client.dart';
import 'package:session_repository/session_repository.dart';
import 'package:user_repository/user_repository.dart';
import 'package:very_good_hub_api/middlewares/middlewares.dart';
import 'package:very_good_hub_api/models/models.dart';

Handler middleware(Handler handler) {
return handler
.use(
bearerAuthentication<ApiSession>(
authenticator: (context, token) async {
final sessionRepository = context.read<SessionRepository>();
final session = await sessionRepository.sessionFromToken(token);

if (session != null) {
final userRepository = context.read<UserRepository>();
final user = await userRepository.findUserById(session.userId);

if (user != null) {
return ApiSession(user: user, session: session);
}
}

return null;
},
),
)
.use(authenticationValidator())
.use(corsHeaders())
.use(requestLogger())
.use(
Expand Down
6 changes: 5 additions & 1 deletion api/routes/hub/session.dart
@@ -1,5 +1,6 @@
import 'dart:io';

import 'package:authentication_repository/authentication_repository.dart';
import 'package:dart_frog/dart_frog.dart';
import 'package:very_good_hub_api/models/models.dart';

Expand All @@ -12,5 +13,8 @@ Future<Response> onRequest(RequestContext context) async {

Future<Response> _onGet(RequestContext context) async {
final apiSession = context.read<ApiSession>();
return Response.json(body: apiSession.session.toJson());
final authenticationRepository = context.read<AuthenticationRepository>();

final token = authenticationRepository.sign(apiSession.session.toJson());
return Response.json(body: {'token': token});
}