using Fido2NetLib;
namespace Fido2SimpleSample.Models
{
public class DFido2User: Fido2User
{
public string Pass { get; set; }
}
}
3. 註冊用戶的部分,其實也沒啥好說的,中間我也沒用到任何資料庫單純寫檔案紀錄資料
[HttpPost]
[Route("registuser")]
[Produces("application/json")]
public IActionResult RegistUser([FromForm] string userid,
[FromForm] string displayName,
[FromForm] string userpass)
{
try
{
if (string.IsNullOrEmpty(userid))
{
return Ok(new CredentialCreateOptions { Status = "error", ErrorMessage = "userid cannot be null" });
}
var user = AuthStorageUtil.GetUserById(userid);
if (user == null)
{
user = new DFido2User
{
DisplayName = displayName,
Name = userid,
Id = Encoding.UTF8.GetBytes(userid),
Pass = AuthStorageUtil.GetMD5(System.Text.Encoding.UTF8.GetBytes(userpass))
};
AuthStorageUtil.SaveUserData(user);
return Ok(new CredentialCreateOptions { Status = "ok" });
}
else
{
return Ok(new CredentialCreateOptions { Status = "error", ErrorMessage = "regist already." });
}
}
catch (Exception e)
{
return Ok(new CredentialCreateOptions { Status = "error", ErrorMessage = e.Message });
}
}
4.登入會員+綁定裝置,其實在後端在登入後會產生一個新的憑證放在 session ( fido2.attestationOptions ) 中,當然這邊你可以改寫到 redis 或是其他Temp DB
JS Code:
$('#login').click(async function (event) {
event.preventDefault();
var userid = $('#userId').val();
var userpass = $('#userPass').val();
// possible values: none, direct, indirect
let attestation_type = "none";
// possible values: , platform, cross-platform
let authenticator_attachment = "";
// possible values: preferred, required, discouraged
let user_verification = "preferred";
// possible values: true,false
let require_resident_key = false;
// prepare form post data
var data = new FormData();
data.append('userid', userid);
data.append('userpass', userpass);
data.append('attType', attestation_type);
data.append('authType', authenticator_attachment);
data.append('userVerification', user_verification);
data.append('requireResidentKey', require_resident_key);
// send to server for registering
let makeCredentialOptions;
try {
makeCredentialOptions = await fetchMakeCredentialOptions(data);
} catch (e) {
console.error(e);
let msg = "Something wen't really wrong";
showErrorAlert(msg);
}
console.log("Credential Options Object", makeCredentialOptions);
if (makeCredentialOptions.status !== "ok") {
console.log("Error creating credential options");
console.log(makeCredentialOptions.errorMessage);
showErrorAlert(makeCredentialOptions.errorMessage);
return;
}
// Turn the challenge back into the accepted format of padded base64
makeCredentialOptions.challenge = coerceToArrayBuffer(makeCredentialOptions.challenge);
// Turn ID into a UInt8Array Buffer for some reason
makeCredentialOptions.user.id = coerceToArrayBuffer(makeCredentialOptions.user.id);
makeCredentialOptions.excludeCredentials = makeCredentialOptions.excludeCredentials.map((c) => {
c.id = coerceToArrayBuffer(c.id);
return c;
});
if (makeCredentialOptions.authenticatorSelection.authenticatorAttachment === null) makeCredentialOptions.authenticatorSelection.authenticatorAttachment = undefined;
console.log("Credential Options Formatted", makeCredentialOptions);
Swal.fire({
title: 'Registering...',
text: 'Tap your security key to finish registration.',
imageUrl: "/images/securitykey.min.svg",
showCancelButton: true,
showConfirmButton: false,
focusConfirm: false,
focusCancel: false
});
//Part 2
console.log("Creating PublicKeyCredential...");
let newCredential;
try {
newCredential = await navigator.credentials.create({
publicKey: makeCredentialOptions
});
} catch (e) {
var msg = "Could not create credentials in browser. Probably because the username is already registered with your authenticator. Please change username or authenticator."
console.error(msg, e);
showErrorAlert(msg, e);
}
console.log("PublicKeyCredential Created", newCredential);
try {
registerNewCredential(newCredential);
} catch (e) {
showErrorAlert(err.message ? err.message : err);
}
});
async function fetchMakeCredentialOptions(formData) {
//formData.set('authType', 'platform')
// formData.set('authType', 'cross-platform');
let response = await fetch('/api/auth/makeCredentialOptions', {
method: 'POST', // or 'PUT'
body: formData, // data can be `string` or {object}!
headers: {
'Accept': 'application/json'
}
});
let data = await response.json();
return data;
}
// This should be used to verify the auth data with the server
async function registerNewCredential(newCredential) {
// Move data into Arrays incase it is super long
let attestationObject = new Uint8Array(newCredential.response.attestationObject);
let clientDataJSON = new Uint8Array(newCredential.response.clientDataJSON);
let rawId = new Uint8Array(newCredential.rawId);
const data = {
id: newCredential.id,
rawId: coerceToBase64Url(rawId),
type: newCredential.type,
extensions: newCredential.getClientExtensionResults(),
response: {
AttestationObject: coerceToBase64Url(attestationObject),
clientDataJson: coerceToBase64Url(clientDataJSON)
}
};
let response;
try {
response = await registerCredentialWithServer(data);
} catch (e) {
showErrorAlert(e);
}
console.log("Credential Object", response);
// show error
if (response.status !== "ok") {
console.log("Error creating credential");
console.log(response.errorMessage);
showErrorAlert(response.errorMessage);
return;
}
// show success
Swal.fire({
title: 'Registration Successful!',
text: 'You\'ve registered successfully.',
type: 'success',
timer: 2000
});
// redirect to dashboard?
//window.location.href = "/dashboard/" + state.user.displayName;
}
async function registerCredentialWithServer(formData) {
let response = await fetch('/api/auth/makeCredential', {
method: 'POST', // or 'PUT'
body: JSON.stringify(formData), // data can be `string` or {object}!
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
});
let data = await response.json();
return data;
}
C# Code:
[HttpPost]
[Route("makeCredentialOptions")]
[Produces("application/json")]
public IActionResult MakeCredentialOptions([FromForm] string userid,
[FromForm] string userpass,
[FromForm] string attType,
[FromForm] string authType,
[FromForm] bool requireResidentKey,
[FromForm] string userVerification)
{
try
{
if (string.IsNullOrEmpty(userid))
{
return Ok(new CredentialCreateOptions { Status = "error", ErrorMessage = "user id null." });
}
if (string.IsNullOrEmpty(userpass))
{
return Ok(new CredentialCreateOptions { Status = "error", ErrorMessage = "userpass null." });
}
// 1. Get user from DB by username
var user = AuthStorageUtil.GetUserById(userid);
if (user == null)
{
return Ok(new CredentialCreateOptions { Status = "error", ErrorMessage = "User Not Existed." });
}
if (AuthStorageUtil.GetMD5(System.Text.Encoding.UTF8.GetBytes(userpass)) != user.Pass)
{
return Ok(new CredentialCreateOptions { Status = "error", ErrorMessage = "User Data Error." });
}
// 2. Get user existing keys by username
//var existingKeys = DemoStorage.GetCredentialsByUser(user).Select(c => c.Descriptor).ToList();
var existingKeys = AuthStorageUtil.GetCredentialsByUser(user.Name).Select(e => e.Descriptor).ToList(); ;
// 3. Create options
var authenticatorSelection = new AuthenticatorSelection
{
RequireResidentKey = requireResidentKey,
UserVerification = userVerification.ToEnum()
};
if (!string.IsNullOrEmpty(authType))
authenticatorSelection.AuthenticatorAttachment = authType.ToEnum();
var exts = new AuthenticationExtensionsClientInputs()
{
Extensions = true,
UserVerificationMethod = true,
};
var options = _fido2.RequestNewCredential(user, existingKeys, authenticatorSelection, attType.ToEnum(), exts);
// 4. Temporarily store options, session/in-memory cache/redis/db
HttpContext.Session.SetString("fido2.attestationOptions", options.ToJson());
// 5. return options to client
return Ok(options);
}
catch (Exception e)
{
return Ok(new CredentialCreateOptions { Status = "error", ErrorMessage = e.Message });
// return Problem();
}
}
[HttpPost]
[Route("makeCredential")]
[Produces("application/json")]
public async Task MakeCredential([FromBody] AuthenticatorAttestationRawResponse attestationResponse, CancellationToken cancellationToken)
{
try
{
// 1. get the options we sent the client
var jsonOptions = HttpContext.Session.GetString("fido2.attestationOptions");
var options = CredentialCreateOptions.FromJson(jsonOptions);
// 2. Create callback so that lib can verify credential id is unique to this user
//if (AuthStorageUtil.IsCredentialIdExisted(attestationResponse.Id)) {
// return Ok(new CredentialMakeResult(status: "error", errorMessage:"Credentail Existed", result: null));
//}
IsCredentialIdUniqueToUserAsyncDelegate callback = static async (args, cancellationToken) =>
{
// var users = await DemoStorage.GetUsersByCredentialIdAsync(args.CredentialId, cancellationToken);
if (AuthStorageUtil.IsCredentialIdExisted(args.CredentialId))
return false;
return true;
};
// 2. Verify and make the credentials
var success = await _fido2.MakeNewCredentialAsync(attestationResponse, options, callback, cancellationToken: cancellationToken);
// 3. Store the credentials in db
var storeCredentail = new StoredCredential
{
Descriptor = new PublicKeyCredentialDescriptor(success.Result.CredentialId),
PublicKey = success.Result.PublicKey,
UserHandle = success.Result.User.Id,
SignatureCounter = success.Result.Counter,
CredType = success.Result.CredType,
RegDate = DateTime.Now,
AaGuid = success.Result.Aaguid
};
AuthStorageUtil.SaveStoredCredential(success.Result.User.Name, storeCredentail);
// 4. return "ok" to the client
return Ok(success);
}
catch (Exception e)
{
return Ok(new CredentialMakeResult(status: "error", errorMessage: FormatException(e), result: null));
}
}
private string FormatException(Exception e)
{
return string.Format("{0}{1}", e.Message, e.InnerException != null ? " (" + e.InnerException.Message + ")" : "");
}