-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add catv1 auth token generator (#671)
* feat: add auth token generator * docs: markdown format * chore: lint issues
- Loading branch information
Showing
5 changed files
with
167 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
71 changes: 71 additions & 0 deletions
71
catalyst_voices_packages/catalyst_cardano_serialization/lib/src/rbac/auth_token.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
import 'dart:convert'; | ||
|
||
import 'package:catalyst_cardano_serialization/catalyst_cardano_serialization.dart'; | ||
import 'package:cbor/cbor.dart'; | ||
import 'package:ulid/ulid.dart'; | ||
|
||
/// The Authentication Token is based loosely on JWT. | ||
/// It consists of an Authentication Header attached to every authenticated | ||
/// request, and an encoded signed. | ||
/// | ||
/// This token can be attached to either individual HTTP requests, | ||
/// or to the beginning of a web socket connection. | ||
/// | ||
/// The authentication header is in the format: | ||
/// | ||
/// ```http | ||
/// Authorization: Bearer catv1.<encoded token> | ||
/// ``` | ||
/// | ||
/// ### Encoded Binary Token Format | ||
/// | ||
/// The Encoded Binary Token is a [CBOR sequence] that consists of 3 fields. | ||
/// | ||
/// * `kid` : The key identifier. | ||
/// * `ulid` : A ULID which defines when the token was issued, | ||
/// and a random nonce. | ||
/// * `signature` : The signature over the `kid` and `ulid` fields. | ||
final class AuthToken { | ||
/// The token prefix which distinguishes this auth token from other | ||
/// auth tokens and allows version via the v{} part. | ||
static const String prefix = 'catv1'; | ||
|
||
/// Prevent creating instances. | ||
const AuthToken._(); | ||
|
||
/// Generates a new auth token at a given [timestamp]. | ||
/// | ||
/// * The [kid] in most cases is going to be a [CertificateHash] | ||
/// of the [privateKey] certificate. | ||
/// * The [privateKey] must correspond to the [kid] specified. | ||
/// * The [timestamp] is a [DateTime] when a given token has been generated. | ||
static Future<String> generate({ | ||
required CertificateHash kid, | ||
required Ed25519PrivateKey privateKey, | ||
required DateTime timestamp, | ||
}) async { | ||
final ulid = CborBytes( | ||
Ulid(millis: timestamp.millisecondsSinceEpoch).toBytes(), | ||
); | ||
|
||
final toBeSigned = CborBytes( | ||
cbor.encode( | ||
CborList([ | ||
kid.toCbor(), | ||
ulid, | ||
]), | ||
), | ||
); | ||
|
||
final signature = await privateKey.sign(cbor.encode(toBeSigned)); | ||
|
||
final cborToken = [ | ||
...cbor.encode(kid.toCbor()), | ||
...cbor.encode(ulid), | ||
...cbor.encode(signature.toCbor()), | ||
]; | ||
|
||
final base64Token = base64Encode(cborToken); | ||
return '$prefix.$base64Token'; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
93 changes: 93 additions & 0 deletions
93
catalyst_voices_packages/catalyst_cardano_serialization/test/rbac/auth_token_test.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
import 'dart:convert'; | ||
|
||
import 'package:catalyst_cardano_serialization/catalyst_cardano_serialization.dart'; | ||
import 'package:cbor/cbor.dart'; | ||
import 'package:cryptography/cryptography.dart'; | ||
import 'package:test/test.dart'; | ||
import 'package:ulid/ulid.dart'; | ||
|
||
// The certificate provided in the request | ||
final _c509Cert = C509Certificate.fromHex( | ||
''' | ||
8B004301F50D6B524643207465737420 | ||
43411A63B0CD001A6955B90047010123 | ||
456789AB01582102B1216AB96E5B3B33 | ||
40F5BDF02E693F16213A04525ED44450 | ||
B1019C2DFD3838AB010058406FC90301 | ||
5259A38C0800A3D0B2969CA21977E8ED | ||
6EC344964D4E1C6B37C8FB541274C3BB | ||
81B2F53073C5F101A5AC2A92886583B6 | ||
A2679B6E682D2A26945ED0B2 | ||
''' | ||
.replaceAll('\n', ''), | ||
); | ||
|
||
void main() { | ||
group(AuthToken, () { | ||
final kid = CertificateHash.fromC509Certificate(_c509Cert); | ||
final privateKey = Ed25519PrivateKey.seeded(0); | ||
final timestamp = DateTime.utc(2023); | ||
|
||
test('Generate AuthToken', () async { | ||
final token = await AuthToken.generate( | ||
kid: kid, | ||
privateKey: privateKey, | ||
timestamp: timestamp, | ||
); | ||
|
||
expect(token, startsWith('${AuthToken.prefix}.')); | ||
|
||
// Decode the base64 token part | ||
final parts = token.split('.'); | ||
expect(parts.length, 2); | ||
|
||
final base64Token = parts[1]; | ||
final decodedToken = base64Decode(base64Token); | ||
|
||
final decodedTokenAsArray = [ | ||
0x83, | ||
...decodedToken, | ||
]; | ||
|
||
// Decode the CBOR token | ||
final decodedCbor = cbor.decode(decodedTokenAsArray) as CborList; | ||
expect(decodedCbor.length, 3); | ||
|
||
final decodedKid = decodedCbor[0] as CborBytes; | ||
final decodedUlid = decodedCbor[1] as CborBytes; | ||
final decodedSignature = decodedCbor[2] as CborBytes; | ||
|
||
expect(decodedKid.bytes, (kid.toCbor() as CborBytes).bytes); | ||
expect( | ||
Ulid.fromBytes(decodedUlid.bytes).toMillis(), | ||
timestamp.millisecondsSinceEpoch, | ||
); | ||
|
||
// Verify the signature | ||
final toBeSigned = CborBytes( | ||
cbor.encode( | ||
CborList([ | ||
kid.toCbor(), | ||
decodedUlid, | ||
]), | ||
), | ||
); | ||
|
||
final toBeSignedEncoded = cbor.encode(toBeSigned); | ||
final publicKey = await privateKey.derivePublicKey(); | ||
|
||
final isValid = await Ed25519().verify( | ||
toBeSignedEncoded, | ||
signature: Signature( | ||
decodedSignature.bytes, | ||
publicKey: SimplePublicKey( | ||
publicKey.bytes, | ||
type: KeyPairType.ed25519, | ||
), | ||
), | ||
); | ||
|
||
expect(isValid, isTrue); | ||
}); | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters