|
|
преди 2 години | |
|---|---|---|
| Properties | преди 2 години | |
| data | преди 2 години | |
| requests | преди 2 години | |
| wwwroot | преди 2 години | |
| .gitignore | преди 2 години | |
| Base64UrlConverter.cs | преди 2 години | |
| CredentialManager.cs | преди 2 години | |
| Folder.DotSettings | преди 2 години | |
| JwtConfig.cs | преди 2 години | |
| KeyConverter.cs | преди 2 години | |
| NameTransform.cs | преди 2 години | |
| OptionsCache.cs | преди 2 години | |
| OptionsWithName.cs | преди 2 години | |
| Passwordless.csproj | преди 2 години | |
| Passwordless.http | преди 2 години | |
| Program.cs | преди 2 години | |
| README.md | преди 2 години | |
| appsettings.Development.json | преди 2 години | |
| appsettings.json | преди 2 години | |
| demo-jwk.json | преди 2 години |
# Overview This is a very basic demo of passwordless authentication in the web, also known as "WebAuthn".
Note that for convenience, the client always sends the base64URL encoded user name from the UI to
the server. This encoded version is used as the Fido2User.Id while the decoded version is stored
as the Fido2User.DisplayName and the Fido2User.Name is then derived from the display name by
converting all characters to lower case and replacing "offending" file name characters with "_".
CredentialCreateOptions generated by the program looks like this:
The create options have to be post-processed on the client before they can be used with
navigator.credentials.create by converting base64URL encoded strings to byte arrays (Uint8Array)
{
"rp": {
"id": "localhost",
"name": "FIDO2 Test"
},
"user": {
"name": "test osteron",
"id": "VGVzdCBPc3Rlcm9u",
"displayName": "Test Osteron"
},
"challenge": "UkTN1q5kjoWcHOFTB6AZWQ",
"pubKeyCredParams": [
{
"type": "public-key",
"alg": -7
},
{
"type": "public-key",
"alg": -257
},
{
"type": "public-key",
"alg": -37
},
{
"type": "public-key",
"alg": -35
},
{
"type": "public-key",
"alg": -258
},
{
"type": "public-key",
"alg": -38
},
{
"type": "public-key",
"alg": -36
},
{
"type": "public-key",
"alg": -259
},
{
"type": "public-key",
"alg": -39
},
{
"type": "public-key",
"alg": -8
}
],
"timeout": 60000,
"attestation": "none",
"authenticatorSelection": {
"requireResidentKey": false,
"userVerification": "discouraged"
},
"excludeCredentials": [],
"extensions": {
"exts": true,
"uvm": false
},
"status": "ok",
"errorMessage": ""
}
The result of calling navigator.credentials.create with the options & challange from the server
is an object that mostly contains binary data:
id and rawId fields represent the same Credential ID, the raw version is an array buffer
while the "plain" version is a base64URL encoded representation. The (plain / non-raw) ID of the credential looks
like: BKbtxxiJoPWfT8x_3fUwlzXYIR6OwRXSQGH-FMykKcthocRhAznj8DMNY-2YZw7By-HNnEJa1CxjTPK0WyzjwQauthenticatorAttachment declares what type of authenticator provided the credential
("cross-platform" meaning a roaming authenticator)type being the type of the credential that was generated (generally "public-key")response.attestationObject is a CBOR encoded ArrayBuffer which
contains the authenticator data and attestation (if present)response.clientDataJSON contains some... client data including the challenge value that was
used and the RP origin
{
"type": "webauthn.create",
"challenge": "NwZKS4GobKzOqa5YvPPD2g",
"origin": "http://localhost:5172",
"crossOrigin": false
}
We again need to do some reformatting, this time from byte arrays to base64URL encoded strings and then the data from the authenticator is directly sent to the server for verification.
{
"id": "BKbtxxiJoPWfT8x_3fUwlzXYIR6OwRXSQGH-FMykKcthocRhAznj8DMNY-2YZw7By-HNnEJa1CxjTPK0WyzjwQ",
"rawId": "BKbtxxiJoPWfT8x/3fUwlzXYIR6OwRXSQGH+FMykKcthocRhAznj8DMNY+2YZw7By+HNnEJa1CxjTPK0WyzjwQ==",
"type": "public-key",
"extensions": {},
"response": {
"AttestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjESZYN5YgOjGh0NBcPZHZgW4/krrmihjLHmVzzuoMdl2NFAAAABAAAAAAAAAAAAAAAAAAAAAAAQASm7ccYiaD1n0/Mf931MJc12CEejsEV0kBh/hTMpCnLYaHEYQM54/AzDWPtmGcOwcvhzZxCWtQsY0zytFss48GlAQIDJiABIVggrEeUH2MVMs5oI0dZOGu9Sm9w/5iMFMRXczBtsDrmSOgiWCBO1F75pFRnZS6wRC3LIvt2U7C10i0gQd73NRG3A38bZA==",
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiTndaS1M0R29iS3pPcWE1WXZQUEQyZyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6NTE3MiIsImNyb3NzT3JpZ2luIjpmYWxzZX0=",
"transports": []
}
}
The server has to compare this response with the challenge from the creation options (the creation options are cached on the server or persisted in the session). Once successful, the credential is persisted in a file "[username].json".
For the login, we create a AssertionOptions instance from a registered login name (if that login
actually exists) which looks like this:
{
"challenge": "VlXKhoI7x3T5LxsKP_WPnw",
"timeout": 60000,
"rpId": "demo.larcanum.net",
"allowCredentials": [
{
"type": "public-key",
"id": "vHJFHVmoY37hrUHFObgkw7QkBB1p44uhYlLc4i9r1BHde5XEVObMB8QOKWKpcf4eWEODN2kK5x84pRlNn8etlQ"
}
],
"userVerification": "discouraged",
"extensions": {
"exts": true,
"uvm": false
},
"status": "ok",
"errorMessage": ""
}
The options again need to be post-processed on the client before they can be used with
navigator.credentials.get by converting base64URL encoded strings to byte arrays (Uint8Array).
The result of navigator.credentials.get looks very similar to the one from
navigator.credentials.create but with the addition of a "signature"
signature field contains the signed challenge from the request object and is the critical
piece of information that the server needs to verifyThe raw verification request looks like this:
{
"id": "vHJFHVmoY37hrUHFObgkw7QkBB1p44uhYlLc4i9r1BHde5XEVObMB8QOKWKpcf4eWEODN2kK5x84pRlNn8etlQ",
"rawId": "vHJFHVmoY37hrUHFObgkw7QkBB1p44uhYlLc4i9r1BHde5XEVObMB8QOKWKpcf4eWEODN2kK5x84pRlNn8etlQ==",
"type": "public-key",
"extensions": {},
"response": {
"authenticatorData": "oRsUvdxlEqdT6Qnbe1LQ/aqF8rHBT3MzNqBH6nFOcf0BAAAACA==",
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiVmxYS2hvSTd4M1Q1THhzS1BfV1BudyIsIm9yaWdpbiI6Imh0dHBzOi8vZGVtby5sYXJjYW51bS5uZXQiLCJjcm9zc09yaWdpbiI6ZmFsc2UsIm90aGVyX2tleXNfY2FuX2JlX2FkZGVkX2hlcmUiOiJkbyBub3QgY29tcGFyZSBjbGllbnREYXRhSlNPTiBhZ2FpbnN0IGEgdGVtcGxhdGUuIFNlZSBodHRwczovL2dvby5nbC95YWJQZXgifQ==",
"signature": "MEUCIBanHNIJS6ozJe0Nzf0fMJDEeqr/R2J33izif54zuGH1AiEAyOxPUhYfd/7vWnIDh1a4Dind3dTPPjE5O02olEhLbUE="
}
}
Once the credentials have been verified on the server, it generates a JWT for those credentials which can be used to access a simple API protected with JWT bearer authentication.
Currently, we directly store the AttestationVerificationSuccess that the client sends to complete
the registration process.
my username.json