Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
3 changes: 3 additions & 0 deletions backends/carp_webservices/lib/carp_auth/carp_auth.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
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';
import 'package:oidc_default_store/oidc_default_store.dart';
import 'package:flutter_web_auth_2/flutter_web_auth_2.dart';

part 'oauth.dart';
part 'carp_user.dart';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ class CarpAuthProperties {
/// Redirect URI for OAuth
final Uri redirectURI;

/// Redirect URI for OAuth
Uri? anonymousRedirectURI;

/// Redirect uri for OAuth after logout
/// If not specified, the [redirectURI] is used.
Uri? logoutRedirectURI;
Expand All @@ -39,6 +42,7 @@ class CarpAuthProperties {
required this.clientId,
this.clientSecret,
required this.redirectURI,
this.anonymousRedirectURI,
required this.discoveryURL,
this.studyDeploymentId,
this.studyId,
Expand Down
89 changes: 89 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,65 @@ class CarpAuthService {
);
}

Future<CarpUser> authenticateWithMagicLink(String uri) async {
assert(_manager != null, 'Manager not configured. Call configure() first.');
if (!_manager!.didInit) await initManager();

String? code;
String? clientId = _authProperties?.clientId;
String? redirectUri = _authProperties?.anonymousRedirectURI?.toString();
TokenResponse tokenResponse = await FlutterWebAuth2.authenticate(
url: uri,
callbackUrlScheme: redirectUri!.split(':/').first,
options: FlutterWebAuth2Options(
intentFlags: ephemeralIntentFlags,
preferEphemeral: true,
)).then((result) async {
code = Uri.parse(result).queryParameters['code'];
if ((_currentUser == null || _currentUser!.isAuthenticated) &&
code != null) {
return await FlutterAppAuth().token(
TokenRequest(
clientId!,
redirectUri,
authorizationCode: code,
discoveryUrl: _authProperties?.discoveryURL.replace(
pathSegments: [
...?_authProperties?.discoveryURL.pathSegments,
'.well-known',
'openid-configuration'
],
).toString(),
grantType: 'authorization_code',
),
);
}
return Future.error("No code in redirect URI");
});
_currentUser = getCurrentUserProfileFromTokenResponse(tokenResponse);

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

if (_currentUser != null) {
_currentUser!.authenticated(OAuthToken(accessToken ?? '',
refreshToken ?? '', idToken ?? '', expiresAt, scope, idToken ?? ''));
_authEventController.add(AuthEvent.authenticated);
return currentUser;
}
_authEventController.add(AuthEvent.failed);
throw CarpServiceException(
httpStatus: HTTPStatus(401),
message: 'Authentication failed.',
);
}

/// 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 +331,36 @@ 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';
final scopeString = tokenResponse.tokenAdditionalParameters?['scope'] ??
tokenResponse.tokenType;
final scope = (scopeString is String) ? scopeString.split(' ') : <String>[];

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

final jwt = JwtDecoder.decode(accessToken);
final expiresAt = tokenResponse.accessTokenExpirationDateTime ??
DateTime.now().add(const Duration(hours: 1));

final oauthToken = OAuthToken(
accessToken,
refreshToken ?? '',
tokenType,
expiresAt,
scope,
idToken ?? '',
);

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
8 changes: 5 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,10 @@ 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
flutter_web_auth_2: ^4.1.0

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