Skip to content
Draft
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
48 changes: 48 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,27 +1,75 @@
.DS_Store
.atom/
.idea/
.vscode/

.packages
.pub/
.dart_tool/
pubspec.lock
pubspec_overrides.local.yaml
flutter_export_environment.sh
coverage
coverage/

*.iml
*.ipr
*.iws

.flutter-plugins
.flutter-plugins-dependencies
devtools_options.yaml

.project
.classpath
.settings
.last_build_id
.metadata

# Local planning notes
backlog.md
task-list.md
todo.md
todos.md

# Dart / Flutter generated docs and build output
doc/api/
build/
.dart_tool/

# Flutter example generated platform files
packages/**/example/.metadata
packages/**/example/.flutter-plugins
packages/**/example/.flutter-plugins-dependencies
packages/**/example/devtools_options.yaml
packages/**/example/build/
packages/**/example/ios/Flutter/.last_build_id
packages/**/example/ios/Flutter/Generated.xcconfig
packages/**/example/ios/Flutter/flutter_export_environment.sh
packages/**/example/ios/Flutter/ephemeral/
packages/**/example/ios/Flutter/Flutter.framework
packages/**/example/ios/Flutter/Flutter.podspec
packages/**/example/ios/Flutter/App.framework
packages/**/example/ios/Flutter/flutter_assets/
packages/**/example/ios/Pods/
packages/**/example/ios/.symlinks/
packages/**/example/ios/Runner/GeneratedPluginRegistrant.*
packages/**/example/ios/Podfile.lock
packages/**/example/android/.gradle/
packages/**/example/android/local.properties
packages/**/example/android/app/debug/
packages/**/example/android/app/profile/
packages/**/example/android/app/release/
packages/**/example/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java
packages/**/example/android/app/src/main/kotlin/io/flutter/plugins/GeneratedPluginRegistrant.kt

# Misc
.env.local
.env.development.local
.env.test.local
.env.production.local
.melos_tool

# AI
docs/superpowers/

8 changes: 8 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "nhost_flutter",
"cwd": "packages/nhost_flutter/example",
"program": "lib/main.dart",
"request": "launch",
"flutterMode": "debug",
"type": "dart"
},
{
"name": "nhost_flutter_auth",
"cwd": "packages/nhost_flutter_auth/example",
Expand Down
2 changes: 1 addition & 1 deletion packages/nhost_auth_dart/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,5 @@ void main() async {

```yaml
dependencies:
nhost_auth_dart: ^2.0.0
nhost_auth_dart: ^2.6.1
```
157 changes: 155 additions & 2 deletions packages/nhost_auth_dart/lib/src/auth_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -805,6 +805,152 @@ class NhostAuthClient implements HasuraAuthClient {

//#endregion

//#region PAT, fetchUser, verifyToken

/// Signs in using a Personal Access Token (PAT).
@override
Future<AuthResponse> signInWithPat(String pat) async {
log.finer('Attempting sign in (PAT)');
AuthResponse? res;
try {
res = await _apiClient.post(
'/signin/pat',
jsonBody: {'personalAccessToken': pat},
responseDeserializer: AuthResponse.fromJson,
);
} catch (e, st) {
log.finer('Sign in (PAT) failed', e, st);
await clearSession();
rethrow;
}
log.finer('Sign in (PAT) successful');
await setSession(res!.session!);
return res;
}

/// Fetches the current user's profile from the server.
@override
Future<User> fetchUser() async {
log.finer('Fetching current user');
final user = await _apiClient.get<User>(
'/user',
responseDeserializer: User.fromJson,
headers: _session.authenticationHeaders,
);
_currentUser = user;
return user;
}

/// Verifies whether [accessToken] is still valid on the server.
@override
Future<bool> verifyToken(String accessToken) async {
log.finer('Verifying token');
try {
await _apiClient.post<void>(
'/token/verify',
headers: {'Authorization': 'Bearer $accessToken'},
);
return true;
} on ApiException {
return false;
}
}

//#endregion

//#region WebAuthn — platform-dependent stubs

@override
Future<Map<String, dynamic>> signInWithWebAuthn() async {
return _apiClient.post(
'/signin/webauthn',
responseDeserializer: (json) => Map<String, dynamic>.from(json as Map),
);
}

@override
Future<AuthResponse> verifyWebAuthnSignIn(
Map<String, dynamic> assertionResponse,
) async {
log.finer('Attempting WebAuthn sign-in verification');
final res = await _apiClient.post(
'/signin/webauthn/verify',
jsonBody: assertionResponse,
responseDeserializer: AuthResponse.fromJson,
);
await setSession(res.session!);
return res;
}

@override
Future<Map<String, dynamic>> signUpWithWebAuthn({String? email}) async {
return _apiClient.post(
'/signup/webauthn',
jsonBody: {if (email != null) 'email': email},
responseDeserializer: (json) => Map<String, dynamic>.from(json as Map),
);
}

@override
Future<AuthResponse> verifyWebAuthnSignUp(
Map<String, dynamic> attestationResponse,
) async {
log.finer('Attempting WebAuthn sign-up verification');
final res = await _apiClient.post(
'/signup/webauthn/verify',
jsonBody: attestationResponse,
responseDeserializer: AuthResponse.fromJson,
);
if (res.session != null) await setSession(res.session!);
return res;
}

@override
Future<Map<String, dynamic>> addWebAuthnCredential() async {
return _apiClient.post(
'/user/webauthn/add',
headers: _session.authenticationHeaders,
responseDeserializer: (json) => Map<String, dynamic>.from(json as Map),
);
}

@override
Future<void> verifyAddWebAuthnCredential(
Map<String, dynamic> attestationResponse,
) async {
await _apiClient.post<void>(
'/user/webauthn/verify',
jsonBody: attestationResponse,
headers: _session.authenticationHeaders,
);
}

@override
Future<Map<String, dynamic>> elevateWithWebAuthn() async {
return _apiClient.post(
'/elevate/webauthn',
headers: _session.authenticationHeaders,
responseDeserializer: (json) => Map<String, dynamic>.from(json as Map),
);
}

@override
Future<AuthResponse> verifyWebAuthnElevation(
Map<String, dynamic> assertionResponse,
) async {
log.finer('Attempting WebAuthn elevation verification');
final res = await _apiClient.post(
'/elevate/webauthn/verify',
jsonBody: assertionResponse,
headers: _session.authenticationHeaders,
responseDeserializer: AuthResponse.fromJson,
);
if (res.session != null) await setSession(res.session!);
return res;
}

//#endregion

//#region Token and session Handling

Future<Session> _refreshSession([String? initRefreshToken]) async {
Expand Down Expand Up @@ -857,6 +1003,12 @@ class NhostAuthClient implements HasuraAuthClient {
if (e is ApiException && e.statusCode == unauthorizedStatus) {
log.finest('Unauthorized refresh token. Forcing signout.');
await signOut();
} else {
// Non-401 failure (e.g. network error, 5xx): clearSession is not called,
// so we must manually clear _loading to prevent authenticationState from
// remaining stuck at inProgress forever (issue #180).
_loading = false;
_onAuthStateChanged(authenticationState);
}

log.severe('Exception during token refresh', e, st);
Expand Down Expand Up @@ -963,8 +1115,9 @@ class NhostAuthClient implements HasuraAuthClient {
@override
String toString() {
return {
'accessToken': accessToken,
'refreshToken': _session.session?.refreshToken,
'accessToken': accessToken == null ? null : '<redacted>',
'refreshToken':
_session.session?.refreshToken == null ? null : '<redacted>',
'accessTokenExpiresIn': _session.session?.accessTokenExpiresIn,
'userEmail': _session.session?.user?.email,
}.toString();
Expand Down
4 changes: 2 additions & 2 deletions packages/nhost_auth_dart/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
name: nhost_auth_dart
description: Nhost Dart Auth Service SDK
description: Dart client for Nhost Auth with email/password, passwordless, MFA, anonymous sign-in, and session management.
version: 2.6.1
homepage: https://nhost.io
repository: https://github.com/nhost/nhost-dart/tree/main/packages/nhost_sdk
repository: https://github.com/nhost/nhost-dart/tree/main/packages/nhost_auth_dart
issue_tracker: https://github.com/nhost/nhost-dart/issues

environment:
Expand Down
46 changes: 46 additions & 0 deletions packages/nhost_auth_dart/test/auth_client_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import 'package:nhost_auth_dart/nhost_auth_dart.dart';
import 'package:nhost_sdk/nhost_sdk.dart';
import 'package:test/test.dart';

const _accessToken = 'eyJhbGciOiJIUzI1NiJ9.'
'eyJodHRwczovL2hhc3VyYS5pby9qd3QvY2xhaW1zIjp7IngtaGFzdXJhLWFsbG93ZWQtcm9sZXMiOlsidXNlciIsIm1lIl0sIngtaGFzdXJhLWRlZmF1bHQtcm9sZSI6InVzZXIiLCJ4LWhhc3VyYS11c2VyLWlkIjoiNzEwYTgyNjMtNTgyNi00NTYzLWE4YTUtNGUyNzJkNDQxYWVkIiwieC1oYXN1cmEtdXNlci1pc0Fub255bW91cyI6ImZhbHNlIn0sInN1YiI6IjcxMGE4MjYzLTU4MjYtNDU2My1hOGE1LTRlMjcyZDQ0MWFlZCIsImlzcyI6Imhhc3VyYS1hdXRoIiwiaWF0IjoxNjQzMzQ3NzgwLCJleHAiOjE2NDMzNDg2ODB9.'
'xzsBH0p34ynPwaHnNs97gVL5tdrccFOrxosuqBra1iw';
const _refreshToken = 'refresh-token-should-not-be-logged';

void main() {
group('NhostAuthClient', () {
test('does not expose session tokens in string output', () async {
final auth = NhostAuthClient(url: 'http://localhost');
addTearDown(auth.close);

await auth.setSession(
Session(
accessToken: _accessToken,
accessTokenExpiresIn: Duration(seconds: 900),
refreshToken: _refreshToken,
user: User(
id: '710a8263-5826-4563-a8a5-4e272d441aed',
displayName: 'Test User',
locale: 'en',
createdAt: DateTime.utc(2024),
isAnonymous: false,
defaultRole: 'user',
roles: const ['user'],
emailVerified: true,
phoneNumber: '',
phoneNumberVerified: false,
email: 'test@example.com',
),
),
);

final output = auth.toString();

expect(output, isNot(contains(_accessToken)));
expect(output, isNot(contains(_refreshToken)));
expect(output, contains('accessToken: <redacted>'));
expect(output, contains('refreshToken: <redacted>'));
expect(output, contains('test@example.com'));
});
});
}
Loading