Skip to content

Commit

Permalink
feat: exposed CIS token (#530)
Browse files Browse the repository at this point in the history
  • Loading branch information
marco-a-affinidi authored Feb 4, 2025
1 parent d7be35a commit edafaf8
Show file tree
Hide file tree
Showing 8 changed files with 134 additions and 121 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,10 @@ class ConsumerAuthProvider implements ConsumerAuthProviderInterface {
Future<String> fetchConsumerToken() async {
return await ConsumerAuthProviderAbstract.instance.fetchConsumerToken();
}

/// Retrieves a CIS token.
@override
Future<String> fetchCisToken() async {
return await ConsumerAuthProviderAbstract.instance.fetchCisToken();
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
abstract interface class ConsumerAuthProviderInterface {
Future<String> fetchConsumerToken();
Future<String> fetchCisToken();
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
import 'dart:typed_data';

import 'package:affinidi_tdk_cryptography/affinidi_tdk_cryptography.dart';
import 'package:base_codecs/base_codecs.dart';
import 'package:jwt_decoder/jwt_decoder.dart';

import 'consumer_auth_provider_abstract.dart';
import 'consumer_token_provider.dart';
import 'token_provider.dart';

class BaseConsumerAuthProvider implements ConsumerAuthProviderAbstract {
final String _encryptedSeed;
final String _encryptionKey;

late final CryptographyService _cryptographyService;
late final ConsumerTokenProvider _tokenProvider;
late final ConsumerTokenProvider _consumerTokenProvider;
late final CisTokenProvider _cisTokenProvider;
late final Uint8List _seedBytes = hexDecode(_cryptographyService.decryptSeed(
encryptedSeedHex: _encryptedSeed,
encryptionKeyHex: _encryptionKey,
));

String? _consumerToken;

Expand All @@ -20,7 +27,8 @@ class BaseConsumerAuthProvider implements ConsumerAuthProviderAbstract {
}) : _encryptedSeed = encryptedSeed,
_encryptionKey = encryptionKey {
_cryptographyService = CryptographyService();
_tokenProvider = ConsumerTokenProvider();
_consumerTokenProvider = ConsumerTokenProvider();
_cisTokenProvider = CisTokenProvider();
}

@override
Expand All @@ -30,19 +38,19 @@ class BaseConsumerAuthProvider implements ConsumerAuthProviderAbstract {
return _consumerToken!;
}

final seed = _cryptographyService.decryptSeed(
encryptedSeedHex: _encryptedSeed,
encryptionKeyHex: _encryptionKey,
);

_consumerToken = await _tokenProvider.getToken(hexDecode(seed));
_consumerToken = await _consumerTokenProvider.getToken(_seedBytes);

return _consumerToken!;
} catch (e) {
throw Exception('Failed to fetch consumer token');
}
}

@override
Future<String> fetchCisToken() async {
return await _cisTokenProvider.getToken(_seedBytes);
}

bool _isTokenExpired(String token) {
return JwtDecoder.isExpired(token);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
part of 'token_provider.dart';

class CisTokenProvider extends TokenProvider {
static final String _tokenEndpoint = Environment.fetchConsumerCisUrl();

@override
Future<String> getToken(Uint8List seedBytes) async {
final myDiD = _getDID(seedBytes);
final header = _getHeader(_getKid(myDiD));
return await _getJwtToken(seedBytes, header, _tokenEndpoint);
}

Map<String, dynamic> _getHeader(String kid) {
return {'alg': 'ES256K', 'kid': kid, 'typ': 'openid4vci-proof+jwt'};
}
}
Original file line number Diff line number Diff line change
@@ -1,91 +1,32 @@
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
part of 'token_provider.dart';

import 'package:affinidi_tdk_common/affinidi_tdk_common.dart';
import 'package:base_codecs/base_codecs.dart';
import 'package:bip32/bip32.dart';
import 'package:crypto/crypto.dart';
import 'package:dio/dio.dart';
import 'package:uuid/uuid.dart';
import 'package:web3dart/credentials.dart';
import 'package:web3dart/crypto.dart';

class ConsumerTokenProvider {
static const etheriumIdentityKey = "m/44'/60'/0'/0/0";
static final String affConsumerAuthTokenEndpoint =
Environment.fetchConsumerAudienceUrl();
static const secondsBetweenApiAuthRefresh = 300;
class ConsumerTokenProvider extends TokenProvider {
static final String _tokenEndpoint = Environment.fetchConsumerAudienceUrl();

@override
Future<String> getToken(Uint8List seedBytes) async {
final master = BIP32.fromSeed(seedBytes);

final key = master.derivePath(etheriumIdentityKey);
final myDiD = _getDID(key.privateKey!);
final header = json.encode(_getHeader(_getKid(myDiD)));
final payload = json.encode(
_getPayload(myDiD, affConsumerAuthTokenEndpoint),
);
final b64header = _base64Unpadded(base64UrlEncode(utf8.encode(header)));
final b64payload = _base64Unpadded(base64UrlEncode(utf8.encode(payload)));
final msgHashHex =
sha256.convert(utf8.encode("$b64header.$b64payload")).bytes;
final assertion = (key.sign(Uint8List.fromList(msgHashHex)));
final jwt = '$b64header.$b64payload.${_base64Unpadded(
base64UrlEncode(Uint8List.fromList(assertion)),
)}';

final myDiD = _getDID(seedBytes);
final header = _getHeader(_getKid(myDiD));
final jwt = await _getJwtToken(seedBytes, header, _tokenEndpoint);
final token = await _getConsumerToken(jwt, myDiD);
return token;
}

String _getDID(Uint8List privateKey) {
final private = EthPrivateKey.fromHex(bytesToHex(privateKey));
return 'did:key:z${base58BitcoinEncode(
Uint8List.fromList([231, 1] + private.publicKey.getEncoded().toList()),
)}';
}

String _getKid(String did) {
return "$did#${did.substring("did:key:".length)}";
}

Map<String, dynamic> _getHeader(String kid) {
return {'alg': 'ES256K', 'kid': kid};
}

Map<String, dynamic> _getPayload(String did, String tokenEndpoint) {
final issueTimeS =
(DateTime.timestamp().millisecondsSinceEpoch / 1000).floor();
final payload = {
'iss': did,
'sub': did,
'aud': tokenEndpoint,
'jti': const Uuid().v4(),
'exp': issueTimeS + 5 * 60,
'iat': issueTimeS,
};
return payload;
}

String _base64Unpadded(String value) {
if (value.endsWith('==')) return value.substring(0, value.length - 2);
if (value.endsWith('=')) return value.substring(0, value.length - 1);
return value;
}

Future<String> _getConsumerToken(String clientAssertion, String did) async {
final dioInstance = Dio();
final data = {
"grant_type": 'client_credentials',
"client_assertion_type":
'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
"client_assertion_type": 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
"client_assertion": clientAssertion,
"client_id": did,
};

final response = await dioInstance.post(
affConsumerAuthTokenEndpoint,
_tokenEndpoint,
data: data,
options: Options(
contentType: 'application/json',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';

import 'package:affinidi_tdk_common/affinidi_tdk_common.dart';
import 'package:base_codecs/base_codecs.dart';
import 'package:bip32/bip32.dart';
import 'package:crypto/crypto.dart';
import 'package:dio/dio.dart';
import 'package:uuid/uuid.dart';
import 'package:web3dart/credentials.dart';
import 'package:web3dart/crypto.dart';

part 'consumer_token_provider.dart';
part 'cis_token_provider.dart';

abstract class TokenProvider {
static const _etheriumIdentityKey = "m/44'/60'/0'/0/0";

Future<String> getToken(Uint8List seedBytes);

Future<String> _getJwtToken(Uint8List seedBytes, Map<String, dynamic> header, String tokenEndpoint) async {
final myDiD = _getDID(seedBytes);
final jsonHeader = json.encode(header);
final payload = json.encode(
_getPayload(myDiD, tokenEndpoint),
);
final b64header = _base64Unpadded(base64UrlEncode(utf8.encode(jsonHeader)));
final b64payload = _base64Unpadded(base64UrlEncode(utf8.encode(payload)));
final msgHashHex = sha256.convert(utf8.encode("$b64header.$b64payload")).bytes;

final walletKey = _getKey(seedBytes);
final assertion = (walletKey.sign(Uint8List.fromList(msgHashHex)));
return '$b64header.$b64payload.${_base64Unpadded(
base64UrlEncode(Uint8List.fromList(assertion)),
)}';
}

String _getDID(Uint8List seedBytes) {
final key = _getKey(seedBytes);
final private = EthPrivateKey.fromHex(bytesToHex(key.privateKey!));
return 'did:key:z${base58BitcoinEncode(
Uint8List.fromList([231, 1] + private.publicKey.getEncoded().toList()),
)}';
}

BIP32 _getKey(Uint8List seedBytes) {
final master = BIP32.fromSeed(seedBytes);
return master.derivePath(_etheriumIdentityKey);
}

String _getKid(String did) {
return "$did#${did.substring("did:key:".length)}";
}

String _base64Unpadded(String value) {
if (value.endsWith('==')) return value.substring(0, value.length - 2);
if (value.endsWith('=')) return value.substring(0, value.length - 1);
return value;
}

Map<String, dynamic> _getPayload(String did, String tokenEndpoint) {
final issueTimeS = (DateTime.timestamp().millisecondsSinceEpoch / 1000).floor();
final payload = {
'iss': did,
'sub': did,
'aud': tokenEndpoint,
'jti': const Uuid().v4(),
'exp': issueTimeS + 5 * 60,
'iat': issueTimeS,
};
return payload;
}
}

This file was deleted.

15 changes: 11 additions & 4 deletions tests/integration/dart/test/consumer_auth_provider_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,22 @@ void main() {
encryptedSeed: env.encryptedSeed,
encryptionKey: env.encryptionKey,
);
final consumerAuthToken1 =
await consumerAuthProvider.fetchConsumerToken();
final consumerAuthToken1 = await consumerAuthProvider.fetchConsumerToken();
expect(consumerAuthToken1, isNotEmpty);

final consumerAuthToken2 =
await consumerAuthProvider.fetchConsumerToken();
final consumerAuthToken2 = await consumerAuthProvider.fetchConsumerToken();
expect(consumerAuthToken2, equals(consumerAuthToken1));
});

test('obtain cis scoped token', () async {
final consumerAuthProvider = ConsumerAuthProvider(
encryptedSeed: env.encryptedSeed,
encryptionKey: env.encryptionKey,
);
final cisAuthToken = await consumerAuthProvider.fetchCisToken();
expect(cisAuthToken, isNotEmpty);
});

// test('identify expired consumer scoped token and request a new one',
// () async {
// final expiredToken = "";
Expand Down

0 comments on commit edafaf8

Please sign in to comment.