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

auth mechanism update #12

Merged
merged 3 commits into from
Oct 22, 2024
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions lib/core/auth/auth_model.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import 'package:freezed_annotation/freezed_annotation.dart';

part 'auth_model.freezed.dart';
part 'auth_model.g.dart';

/// Represents the authentication token for each request.
@freezed
class AuthModel with _$AuthModel {
/// Create a new [AuthModel].
const factory AuthModel({
/// An UUID string, unique per request.
required String token,

/// Unix time in seconds. It is the time when the token was issued.
/// Will expire after a time window.
required int issuedAt,
}) = _AuthModel;

/// Creates a new [AuthModel] from a JSON object.
factory AuthModel.fromJson(Map<String, dynamic> json) =>
_$AuthModelFromJson(json);
}
197 changes: 197 additions & 0 deletions lib/core/auth/auth_model.freezed.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark

part of 'auth_model.dart';

// **************************************************************************
// FreezedGenerator
// **************************************************************************

T _$identity<T>(T value) => value;

final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');

AuthModel _$AuthModelFromJson(Map<String, dynamic> json) {
return _AuthModel.fromJson(json);
}

/// @nodoc
mixin _$AuthModel {
/// An UUID string, unique per request.
String get token => throw _privateConstructorUsedError;

/// Unix time in milliseconds.
/// The time when the token was issued.
/// Will expire after a time window.
int get issuedAt => throw _privateConstructorUsedError;

/// Serializes this AuthModel to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;

/// Create a copy of AuthModel
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$AuthModelCopyWith<AuthModel> get copyWith =>
throw _privateConstructorUsedError;
}

/// @nodoc
abstract class $AuthModelCopyWith<$Res> {
factory $AuthModelCopyWith(AuthModel value, $Res Function(AuthModel) then) =
_$AuthModelCopyWithImpl<$Res, AuthModel>;
@useResult
$Res call({String token, int issuedAt});
}

/// @nodoc
class _$AuthModelCopyWithImpl<$Res, $Val extends AuthModel>
implements $AuthModelCopyWith<$Res> {
_$AuthModelCopyWithImpl(this._value, this._then);

// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;

/// Create a copy of AuthModel
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? token = null,
Object? issuedAt = null,
}) {
return _then(_value.copyWith(
token: null == token
? _value.token
: token // ignore: cast_nullable_to_non_nullable
as String,
issuedAt: null == issuedAt
? _value.issuedAt
: issuedAt // ignore: cast_nullable_to_non_nullable
as int,
) as $Val);
}
}

/// @nodoc
abstract class _$$AuthModelImplCopyWith<$Res>
implements $AuthModelCopyWith<$Res> {
factory _$$AuthModelImplCopyWith(
_$AuthModelImpl value, $Res Function(_$AuthModelImpl) then) =
__$$AuthModelImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({String token, int issuedAt});
}

/// @nodoc
class __$$AuthModelImplCopyWithImpl<$Res>
extends _$AuthModelCopyWithImpl<$Res, _$AuthModelImpl>
implements _$$AuthModelImplCopyWith<$Res> {
__$$AuthModelImplCopyWithImpl(
_$AuthModelImpl _value, $Res Function(_$AuthModelImpl) _then)
: super(_value, _then);

/// Create a copy of AuthModel
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? token = null,
Object? issuedAt = null,
}) {
return _then(_$AuthModelImpl(
token: null == token
? _value.token
: token // ignore: cast_nullable_to_non_nullable
as String,
issuedAt: null == issuedAt
? _value.issuedAt
: issuedAt // ignore: cast_nullable_to_non_nullable
as int,
));
}
}

/// @nodoc
@JsonSerializable()
class _$AuthModelImpl implements _AuthModel {
const _$AuthModelImpl({required this.token, required this.issuedAt});

factory _$AuthModelImpl.fromJson(Map<String, dynamic> json) =>
_$$AuthModelImplFromJson(json);

/// An UUID string, unique per request.
@override
final String token;

/// Unix time in milliseconds.
/// The time when the token was issued.
/// Will expire after a time window.
@override
final int issuedAt;

@override
String toString() {
return 'AuthModel(token: $token, issuedAt: $issuedAt)';
}

@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$AuthModelImpl &&
(identical(other.token, token) || other.token == token) &&
(identical(other.issuedAt, issuedAt) ||
other.issuedAt == issuedAt));
}

@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType, token, issuedAt);

/// Create a copy of AuthModel
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$AuthModelImplCopyWith<_$AuthModelImpl> get copyWith =>
__$$AuthModelImplCopyWithImpl<_$AuthModelImpl>(this, _$identity);

@override
Map<String, dynamic> toJson() {
return _$$AuthModelImplToJson(
this,
);
}
}

abstract class _AuthModel implements AuthModel {
const factory _AuthModel(
{required final String token,
required final int issuedAt}) = _$AuthModelImpl;

factory _AuthModel.fromJson(Map<String, dynamic> json) =
_$AuthModelImpl.fromJson;

/// An UUID string, unique per request.
@override
String get token;

/// Unix time in milliseconds.
/// The time when the token was issued.
/// Will expire after a time window.
@override
int get issuedAt;

/// Create a copy of AuthModel
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$AuthModelImplCopyWith<_$AuthModelImpl> get copyWith =>
throw _privateConstructorUsedError;
}
19 changes: 19 additions & 0 deletions lib/core/auth/auth_model.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions lib/core/auth/simple_auth.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,7 @@ abstract class SimpleAuth {

/// Check if [decryptedToken] is valid.
bool isTokenValid(String decryptedToken);

/// Check if [issuedAt] is expired.
bool isTokenExpired(int issuedAt);
}
20 changes: 19 additions & 1 deletion lib/core/auth/simple_auth_rsa.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'dart:convert';

import 'package:encrypt/encrypt.dart';
import 'package:kuwot_api/core/auth/simple_auth.dart';
import 'package:kuwot_api/core/time.dart';
import 'package:kuwot_api/env.dart';
import 'package:pointycastle/asymmetric/api.dart';
import 'package:uuid/validation.dart';
Expand All @@ -11,9 +12,14 @@ import 'package:uuid/validation.dart';
/// decrypt it with private key and check if the UUID is valid.
class SimpleAuthRSA implements SimpleAuth {
/// Creates a new [SimpleAuthRSA] with the provided [env].
SimpleAuthRSA({required Env env}) : _env = env;
SimpleAuthRSA({
required Env env,
required Time time,
}) : _env = env,
_time = time;

final Env _env;
final Time _time;

@override
String? decryptToken(String token) {
Expand All @@ -38,4 +44,16 @@ class SimpleAuthRSA implements SimpleAuth {
// check if token is a valid UUID
return UuidValidation.isValidUUID(fromString: decryptedToken);
}

@override
bool isTokenExpired(int issuedAt) {
const tokenLifetimeSeconds = 300;
const allowedDriftSeconds = 10;

final now = _time.getUnixTimestamp();
final diff = now - issuedAt;

return diff < -allowedDriftSeconds ||
diff > (tokenLifetimeSeconds + allowedDriftSeconds);
}
}
13 changes: 13 additions & 0 deletions lib/core/time.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/// Humble class for time related operations.
abstract class Time {
/// Get unix timestamp in seconds.
int getUnixTimestamp();
}

/// Implementation of [Time] using [DateTime].
class TimeImpl implements Time {
@override
int getUnixTimestamp() {
return DateTime.now().millisecondsSinceEpoch ~/ 1000;
}
}
10 changes: 9 additions & 1 deletion lib/injection_container.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import 'package:kuwot_api/core/auth/simple_auth.dart';
import 'package:kuwot_api/core/auth/simple_auth_rsa.dart';
import 'package:kuwot_api/core/network/network.dart';
import 'package:kuwot_api/core/time.dart';
import 'package:kuwot_api/data/data_sources/local/quote_local_data_source.dart';
import 'package:kuwot_api/data/data_sources/remote/unsplash_remote_data_source.dart';
import 'package:kuwot_api/data/quote_database.dart';
Expand All @@ -24,6 +25,7 @@ class InjectionContainer {
static final InjectionContainer _instance = InjectionContainer._internal();

Env? _env;
Time? _time;
Network? _network;
SimpleAuth? _simpleAuth;
QuoteDb? _quoteDatabase;
Expand All @@ -34,11 +36,17 @@ class InjectionContainer {
/// [Env] instance, used for environment variables.
Env get env => _env ??= EnvImpl();

/// [Time] instance, used for time operations.
Time get time => _time ??= TimeImpl();

/// [Network] instance, used for network operations.
Network get network => _network ??= NetworkImpl();

/// [SimpleAuth] instance, used for authentication.
SimpleAuth get simpleAuth => _simpleAuth ??= SimpleAuthRSA(env: env);
SimpleAuth get simpleAuth => _simpleAuth ??= SimpleAuthRSA(
env: env,
time: time,
);

/// [QuoteDb] instance, interacts with the quote SQLite database.
QuoteDb get quoteDatabase => _quoteDatabase ??= QuoteDb();
Expand Down
27 changes: 22 additions & 5 deletions routes/_middleware.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import 'dart:convert';

import 'package:dart_frog/dart_frog.dart';
import 'package:dart_frog_auth/dart_frog_auth.dart';
import 'package:kuwot_api/core/auth/auth_model.dart';
import 'package:kuwot_api/core/auth/simple_auth.dart';

import 'package:shelf_cors_headers/shelf_cors_headers.dart' as shelf;
Expand Down Expand Up @@ -32,13 +35,27 @@ Handler _authCheck(Handler handler) {
authenticator: (context, token) async {
final authenticator = context.read<SimpleAuth>();

final decryptedToken = authenticator.decryptToken(token);
if (decryptedToken == null) {
try {
final decryptedToken = authenticator.decryptToken(token);
if (decryptedToken == null) {
throw Exception('Unable to decrypt token');
}

final authJson = jsonDecode(decryptedToken) as Map<String, dynamic>;
final authModel = AuthModel.fromJson(authJson);

final isTokenValid = authenticator.isTokenValid(authModel.token);
final isTokenExpired =
authenticator.isTokenExpired(authModel.issuedAt);

if (isTokenValid && !isTokenExpired) {
return true;
}

throw Exception('Token is invalid or expired');
} on Exception catch (_) {
return null;
}

final isTokenValid = authenticator.isTokenValid(decryptedToken);
return isTokenValid ? true : null;
},
applies: (RequestContext context) async {
return appliedRoutes.any((e) => context.request.uri.path.startsWith(e));
Expand Down
Loading