From 03de78c4fb31f60ac0ed67bcbdbd7be369be09af Mon Sep 17 00:00:00 2001 From: Bibash Shrestha Date: Thu, 19 Dec 2024 18:14:31 +0545 Subject: [PATCH] feat: Remove client id scheme for draft 22 and above #3160 --- .../helper_functions/helper_functions.dart | 22 ++++- .../cubit/qr_code_scan_cubit.dart | 89 +++++++++++++++---- lib/scan/cubit/scan_cubit.dart | 36 ++++++-- lib/splash/bloclisteners/blocklisteners.dart | 9 ++ .../oidc4vc/lib/src/oidc4vp_draft_type.dart | 4 + .../helper_functions_test.dart | 7 +- 6 files changed, 137 insertions(+), 30 deletions(-) diff --git a/lib/app/shared/helper_functions/helper_functions.dart b/lib/app/shared/helper_functions/helper_functions.dart index a2b79e61d..6b035872d 100644 --- a/lib/app/shared/helper_functions/helper_functions.dart +++ b/lib/app/shared/helper_functions/helper_functions.dart @@ -1504,6 +1504,7 @@ Future fetchRequestUriPayload({ String getUpdatedUrlForSIOPV2OIC4VP({ required Uri uri, required Map response, + required String clientId, }) { final responseType = response['response_type']; final redirectUri = response['redirect_uri']; @@ -1511,7 +1512,6 @@ String getUpdatedUrlForSIOPV2OIC4VP({ final responseUri = response['response_uri']; final responseMode = response['response_mode']; final nonce = response['nonce']; - final clientId = response['client_id']; final claims = response['claims']; final stateValue = response['state']; final presentationDefinition = response['presentation_definition']; @@ -1526,7 +1526,7 @@ String getUpdatedUrlForSIOPV2OIC4VP({ queryJson['scope'] = scope; } - if (!uri.queryParameters.containsKey('client_id') && clientId != null) { + if (!uri.queryParameters.containsKey('client_id')) { queryJson['client_id'] = clientId; } @@ -2426,3 +2426,21 @@ bool isContract(String reciever) { if (reciever.startsWith('KT1')) return true; return false; } + +String getClientIdForPresentation({ + required bool draft22AndAbove, + required String? clientId, +}) { + if (clientId == null) return ''; + if (draft22AndAbove) { + final parts = clientId.split(':'); + if (parts.length == 2) { + return parts[1]; + } else { + // this is mistake though + return clientId; + } + } else { + return clientId; + } +} diff --git a/lib/dashboard/qr_code/qr_code_scan/cubit/qr_code_scan_cubit.dart b/lib/dashboard/qr_code/qr_code_scan/cubit/qr_code_scan_cubit.dart index 2e038fa75..84bb4ba05 100644 --- a/lib/dashboard/qr_code/qr_code_scan/cubit/qr_code_scan_cubit.dart +++ b/lib/dashboard/qr_code/qr_code_scan/cubit/qr_code_scan_cubit.dart @@ -200,7 +200,7 @@ class QRCodeScanCubit extends Cubit { if (requestUri != null || request != null) { /// verifier side (oidc4vp) or (siopv2 oidc4vc) with request_uri /// verify the encoded data first - await verifyJWTBeforeLaunchingOIDC4VCANDSIOPV2Flow(); + await verifyJWTBeforeLaunchingOIDC4VPANDSIOPV2Flow(); return; } else { emit(state.acceptHost()); @@ -611,6 +611,20 @@ class QRCodeScanCubit extends Cubit { final String? requestUri = uri.queryParameters['request_uri']; final String? request = uri.queryParameters['request']; + final bool draft22AndAbove = profileCubit + .state + .model + .profileSetting + .selfSovereignIdentityOptions + .customOidc4vcProfile + .oidc4vpDraft + .draft22AndAbove; + + final clientId = getClientIdForPresentation( + draft22AndAbove: draft22AndAbove, + clientId: state.uri!.queryParameters['client_id'], + ); + /// check if request uri is provided or not if (requestUri != null || request != null) { late dynamic encodedData; @@ -634,6 +648,7 @@ class QRCodeScanCubit extends Cubit { final String newUrl = getUpdatedUrlForSIOPV2OIC4VP( uri: uri, response: response, + clientId: clientId, ); emit( @@ -713,8 +728,8 @@ class QRCodeScanCubit extends Cubit { final redirectUri = state.uri!.queryParameters['redirect_uri']; final responseUri = state.uri!.queryParameters['response_uri']; - final clientId = state.uri!.queryParameters['client_id']; - final isClientIdUrl = isURL(clientId.toString()); + + final isClientIdUrl = isURL(clientId); /// id_token only if (isIDTokenOnly(responseType)) { @@ -791,7 +806,7 @@ class QRCodeScanCubit extends Cubit { if (isSecurityHigh && responseUri != null && isClientIdUrl && - !responseUri.contains(clientId.toString())) { + !responseUri.contains(clientId)) { throw ResponseMessage( data: { 'error': 'invalid_request', @@ -806,7 +821,7 @@ class QRCodeScanCubit extends Cubit { if (isSecurityHigh && redirectUri != null && isClientIdUrl && - !redirectUri.contains(clientId.toString())) { + !redirectUri.contains(clientId)) { throw ResponseMessage( data: { 'error': 'invalid_request', @@ -1070,7 +1085,7 @@ class QRCodeScanCubit extends Cubit { } /// verify jwt - Future verifyJWTBeforeLaunchingOIDC4VCANDSIOPV2Flow() async { + Future verifyJWTBeforeLaunchingOIDC4VPANDSIOPV2Flow() async { final String? requestUri = state.uri?.queryParameters['request_uri']; final String? request = state.uri?.queryParameters['request']; @@ -1079,10 +1094,8 @@ class QRCodeScanCubit extends Cubit { if (request != null) { encodedData = request; } else if (requestUri != null) { - encodedData = await fetchRequestUriPayload( - url: requestUri, - client: client, - ); + encodedData = + await fetchRequestUriPayload(url: requestUri, client: client); } final customOidc4vcProfile = profileCubit.state.model.profileSetting @@ -1094,7 +1107,7 @@ class QRCodeScanCubit extends Cubit { final Map payload = jwtDecode.parseJwt(encodedData as String); - final String clientId = payload['client_id'].toString(); + var clientId = payload['client_id'].toString(); //check Signature try { @@ -1117,7 +1130,42 @@ class QRCodeScanCubit extends Cubit { Map? publicKeyJwk; - final clientIdScheme = payload['client_id_scheme']; + var clientIdScheme = payload['client_id_scheme']; + + /// With OIDC4VP Draft 22 and above the client_id_scheme is removed + /// from the authorization request but the value is added to the + /// client_id to be the new client_id value + /// + /// in the client_id Authorization Request parameter and other places + /// where the Client Identifier is used, the Client Identifier Schemes + /// are prefixed to the usual Client Identifier, separated by a : + /// (colon) character: : + + if (clientIdScheme == null) { + final draft22AndAbove = profileCubit + .state + .model + .profileSetting + .selfSovereignIdentityOptions + .customOidc4vcProfile + .oidc4vpDraft + .draft22AndAbove; + + if (draft22AndAbove) { + final parts = clientId.split(':'); + if (parts.length == 2) { + clientIdScheme = parts[0]; + clientId = parts[1]; + } else { + throw ResponseMessage( + data: { + 'error': 'invalid_request', + 'error_description': 'Invalid client_id', + }, + ); + } + } + } if (clientIdScheme != null) { final Map header = @@ -1174,7 +1222,16 @@ class QRCodeScanCubit extends Cubit { final redirectUri = state.uri!.queryParameters['redirect_uri']; final responseUri = state.uri!.queryParameters['response_uri']; - final clientId = state.uri!.queryParameters['client_id'] ?? ''; + final customOidc4vcProfile = profileCubit.state.model.profileSetting + .selfSovereignIdentityOptions.customOidc4vcProfile; + + final bool draft22AndAbove = + customOidc4vcProfile.oidc4vpDraft.draft22AndAbove; + + final clientId = getClientIdForPresentation( + draft22AndAbove: draft22AndAbove, + clientId: state.uri!.queryParameters['client_id'], + ); final nonce = state.uri?.queryParameters['nonce']; final stateValue = state.uri?.queryParameters['state']; @@ -1182,8 +1239,7 @@ class QRCodeScanCubit extends Cubit { // final bool? isEBSI = // await isEBSIForVerifier(client: client, uri: state.uri!); - final didKeyType = profileCubit.state.model.profileSetting - .selfSovereignIdentityOptions.customOidc4vcProfile.defaultDid; + final didKeyType = customOidc4vcProfile.defaultDid; final privateKey = await fetchPrivateKey( profileCubit: profileCubit, @@ -1196,9 +1252,6 @@ class QRCodeScanCubit extends Cubit { didKeyType: didKeyType, ); - final customOidc4vcProfile = profileCubit.state.model.profileSetting - .selfSovereignIdentityOptions.customOidc4vcProfile; - final Map responseData = await oidc4vc.getDataForSiopV2Flow( clientId: clientId, diff --git a/lib/scan/cubit/scan_cubit.dart b/lib/scan/cubit/scan_cubit.dart index 60cdff339..12a2ef357 100644 --- a/lib/scan/cubit/scan_cubit.dart +++ b/lib/scan/cubit/scan_cubit.dart @@ -558,11 +558,17 @@ class ScanCubit extends Cubit { if (responseMode == 'direct_post.jwt') { final iat = (DateTime.now().millisecondsSinceEpoch / 1000).round(); - final clientId = uri.queryParameters['client_id'] ?? ''; - final customOidc4vcProfile = profileCubit.state.model.profileSetting .selfSovereignIdentityOptions.customOidc4vcProfile; + final bool draft22AndAbove = + customOidc4vcProfile.oidc4vpDraft.draft22AndAbove; + + final clientId = getClientIdForPresentation( + draft22AndAbove: draft22AndAbove, + clientId: uri.queryParameters['client_id'], + ); + final didKeyType = customOidc4vcProfile.defaultDid; final (did, _) = await getDidAndKid( @@ -836,10 +842,17 @@ class ScanCubit extends Cubit { VCFormatType? formatFromPresentationSubmission, }) async { final nonce = uri.queryParameters['nonce'] ?? ''; - final clientId = uri.queryParameters['client_id'] ?? ''; - final customOidc4vcProfile = - profileSetting.selfSovereignIdentityOptions.customOidc4vcProfile; + final customOidc4vcProfile = profileCubit.state.model.profileSetting + .selfSovereignIdentityOptions.customOidc4vcProfile; + + final bool draft22AndAbove = + customOidc4vcProfile.oidc4vpDraft.draft22AndAbove; + + final clientId = getClientIdForPresentation( + draft22AndAbove: draft22AndAbove, + clientId: uri.queryParameters['client_id'], + ); if (formatFromPresentationSubmission == VCFormatType.vcSdJWT) { final credentialListJwt = getStringCredentialsForToken( @@ -935,12 +948,19 @@ class ScanCubit extends Cubit { profileCubit: profileCubit, ); - final nonce = uri.queryParameters['nonce'] ?? ''; - final clientId = uri.queryParameters['client_id'] ?? ''; - final customOidc4vcProfile = profileCubit.state.model.profileSetting .selfSovereignIdentityOptions.customOidc4vcProfile; + final bool draft22AndAbove = + customOidc4vcProfile.oidc4vpDraft.draft22AndAbove; + + final clientId = getClientIdForPresentation( + draft22AndAbove: draft22AndAbove, + clientId: uri.queryParameters['client_id'], + ); + + final nonce = uri.queryParameters['nonce'] ?? ''; + final idToken = await oidc4vc.extractIdToken( clientId: clientId, credentialsToBePresented: credentialList, diff --git a/lib/splash/bloclisteners/blocklisteners.dart b/lib/splash/bloclisteners/blocklisteners.dart index cfcb2c4bd..6b7a3283d 100644 --- a/lib/splash/bloclisteners/blocklisteners.dart +++ b/lib/splash/bloclisteners/blocklisteners.dart @@ -331,9 +331,18 @@ final qrCodeBlocListener = BlocListener( token: encodedData as String, ); + final bool draft22AndAbove = + customOidc4vcProfile.oidc4vpDraft.draft22AndAbove; + + final clientId = getClientIdForPresentation( + draft22AndAbove: draft22AndAbove, + clientId: state.uri!.queryParameters['client_id'], + ); + url = getUpdatedUrlForSIOPV2OIC4VP( uri: state.uri!, response: response, + clientId: clientId, ); } diff --git a/packages/oidc4vc/lib/src/oidc4vp_draft_type.dart b/packages/oidc4vc/lib/src/oidc4vp_draft_type.dart index 3c3e9eba3..f4b4eb0d3 100644 --- a/packages/oidc4vc/lib/src/oidc4vp_draft_type.dart +++ b/packages/oidc4vc/lib/src/oidc4vp_draft_type.dart @@ -36,4 +36,8 @@ extension OIDC4VPDraftTypeX on OIDC4VPDraftType { return 'Draft 23'; } } + + bool get draft22AndAbove { + return this == OIDC4VPDraftType.draft22 || this == OIDC4VPDraftType.draft23; + } } diff --git a/test/app/shared/helper_functions/helper_functions_test.dart b/test/app/shared/helper_functions/helper_functions_test.dart index 03c884f34..a24f8e799 100644 --- a/test/app/shared/helper_functions/helper_functions_test.dart +++ b/test/app/shared/helper_functions/helper_functions_test.dart @@ -1058,8 +1058,11 @@ void main() { 'client_metadata_uri': 'https://example.com/client_metadata', }; - final updatedUrl = - getUpdatedUrlForSIOPV2OIC4VP(uri: uri, response: response); + final updatedUrl = getUpdatedUrlForSIOPV2OIC4VP( + uri: uri, + response: response, + clientId: 'client123', + ); expect( updatedUrl.split('&'),