Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
5 changes: 5 additions & 0 deletions backends/carp_webservices/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 3.8.0

* anonymous authentication
* upgrading some packages

## 3.7.0

* fix of issues [#467](https://github.com/cph-cachet/carp.sensing-flutter/issues/467)
Expand Down
1 change: 1 addition & 0 deletions backends/carp_webservices/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:carp_webservices/carp_services/carp_services.dart';
import 'package:carp_webservices/carp_auth/carp_auth.dart';
import 'package:carp_core/carp_core.dart';
import 'package:oidc/oidc.dart';
import 'package:flutter_appauth/flutter_appauth.dart';

void main() {
CarpMobileSensing.ensureInitialized();
Expand Down
2 changes: 1 addition & 1 deletion backends/carp_webservices/example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ dependencies:
carp_webservices:
path: ../

oidc: ^0.9.0+1
oidc: ^0.12.0

dev_dependencies:
test: any
Expand Down
2 changes: 2 additions & 0 deletions backends/carp_webservices/lib/carp_auth/carp_auth.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import 'dart:async';
import 'package:carp_mobile_sensing/carp_mobile_sensing.dart';
import 'package:carp_webservices/carp_services/carp_services.dart';
import 'package:flutter_appauth/flutter_appauth.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:jwt_decoder/jwt_decoder.dart';
import 'package:oidc/oidc.dart';
Expand Down
74 changes: 74 additions & 0 deletions backends/carp_webservices/lib/carp_auth/carp_auth_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,44 @@ class CarpAuthService {
);
}

Future<CarpUser> authenticateWithMagicLink(String code) async {
final TokenResponse tokenResponse = await FlutterAppAuth().token(
TokenRequest(
'studies-app',
'carp-studies:/anonymous',
authorizationCode: code,
discoveryUrl:
'https://dev.carp.dk/auth/realms/Carp/.well-known/openid-configuration',
grantType: 'authorization_code',
),
);

_currentUser = getCurrentUserProfileFromTokenResponse(tokenResponse);

final accessToken = tokenResponse.accessToken;
final refreshToken = tokenResponse.refreshToken;
final idToken = tokenResponse.idToken;
final scopeString = tokenResponse.tokenAdditionalParameters?['scope'] ??
tokenResponse.tokenType; // fallback if needed
final scope = (scopeString is String) ? scopeString.split(' ') : <String>[];
final expiresAt = tokenResponse.accessTokenExpirationDateTime ??
DateTime.now().add(const Duration(hours: 1));

if (_currentUser == null) {
_authEventController.add(AuthEvent.failed);
throw CarpServiceException(
httpStatus: HTTPStatus(401),
message: 'Authentication failed: could not build user profile.',
);
}

_currentUser!.authenticated(OAuthToken(accessToken ?? '',
refreshToken ?? '', idToken ?? '', expiresAt, scope, idToken ?? ''));

_authEventController.add(AuthEvent.authenticated);
return _currentUser!;
}

/// Authenticate to this CARP service using a [username] and [password].
///
/// The discovery URL in the [authProperties] is used to find the Identity Server.
Expand Down Expand Up @@ -272,6 +310,42 @@ class CarpAuthService {
return CarpUser.fromJWT(jwt, user.token);
}

CarpUser? getCurrentUserProfileFromTokenResponse(
TokenResponse tokenResponse) {
final accessToken = tokenResponse.accessToken;
final refreshToken = tokenResponse.refreshToken;
final idToken = tokenResponse.idToken;
final tokenType =
'bearer'; // AppAuth always issues Bearer tokens for OAuth2
final scopeString = tokenResponse.tokenAdditionalParameters?['scope'] ??
tokenResponse.tokenType; // fallback if needed
final scope = (scopeString is String) ? scopeString.split(' ') : <String>[];

if (accessToken == null || accessToken.isEmpty) {
return null;
}

// Decode the JWT to extract user claims
final jwt = JwtDecoder.decode(accessToken);

// Compute expiry from AppAuth info (if not available, fallback to now + 1h)
final expiresAt = tokenResponse.accessTokenExpirationDateTime ??
DateTime.now().add(const Duration(hours: 1));

// Build the OAuthToken with proper field mapping
final oauthToken = OAuthToken(
accessToken,
refreshToken ?? '',
tokenType,
expiresAt,
scope,
idToken ?? '',
);

// Finally, construct CarpUser from the JWT and token
return CarpUser.fromJWTOAuth(jwt, oauthToken);
}

/// Makes sure that the [CarpApp] or [CarpUser] is configured, by throwing a
/// [CarpServiceException] if they are null.
/// Otherwise, returns the non-null value.
Expand Down
13 changes: 13 additions & 0 deletions backends/carp_webservices/lib/carp_auth/carp_user.dart
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,19 @@ class CarpUser {
);
}

factory CarpUser.fromJWTOAuth(Map<String, dynamic> jwt, OAuthToken token) {
return CarpUser(
username: jwt['preferred_username'] as String,
id: jwt['sub'] as String,
firstName: jwt['given_name'] as String?,
lastName: jwt['family_name'] as String?,
email: jwt['email'] as String?,
roles: (jwt['realm_access']?['roles'] as List<dynamic>?) ?? [],
token: token,
);
}


factory CarpUser.fromJson(Map<String, dynamic> json) =>
_$CarpUserFromJson(json);
Map<String, dynamic> toJson() => _$CarpUserToJson(this);
Expand Down
7 changes: 4 additions & 3 deletions backends/carp_webservices/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: carp_webservices
description: Flutter API for accessing the CARP web services - authentication, file management, data points, and app-specific collections of documents.
version: 3.7.0
version: 3.8.0
homepage: https://github.com/cph-cachet/carp.sensing-flutter

environment:
Expand All @@ -25,8 +25,9 @@ dependencies:
meta: ^1.7.0
url_launcher: ^6.0.9
jwt_decoder: ^2.0.1
oidc: ^0.9.0+1
oidc_default_store: ^0.2.0+8
oidc: ^0.12.0
oidc_default_store: ^0.4.0
flutter_appauth: ^9.0.1

# Overriding carp libraries to use the local copy
# Remove this before release of package
Expand Down