diff --git a/auth-new/passkey.go b/auth-new/passkey.go index 28b4fbc..4da055a 100644 --- a/auth-new/passkey.go +++ b/auth-new/passkey.go @@ -15,6 +15,13 @@ import ( "git.mstar.dev/mstar/linstrom/storage-new/models" ) +// Helper for discovering a user during a dicovering login (user not known yet). +// Not thread safe and should only be used once. +// FoundUser will be set after a successful call to userWithPasskeyDiscoverer +type passkeyDiscoverer struct { + FoundUser *models.User +} + // TODO: Check if passkey encryption is viable // Check if encryption for passkey info data is viable to implement // and if we should do it. @@ -25,7 +32,7 @@ import ( // Start the login process via passkey for a given username. // Returns the credential options the passkey needs to sign -func (a *Authenticator) StartPasskeyLogin( +func (a *Authenticator) StartPasskeyLoginWithUsername( username string, ) (*protocol.CredentialAssertion, string, error) { if ok, err := a.canUsernameLogin(username); !ok { @@ -58,7 +65,7 @@ func (a *Authenticator) StartPasskeyLogin( // Complete a passkey login request // Takes the username logging in as well as the raw request containing the passkey response -func (a *Authenticator) CompletePasskeyLogin( +func (a *Authenticator) CompletePasskeyLoginWithUsername( username string, sessionId string, response *http.Request, @@ -132,6 +139,82 @@ func (a *Authenticator) CompletePasskeyLogin( return dbAccessToken.Token, nil } +// Start the login process via passkey for an unknown username. +// Returns the credential options the passkey needs to sign. +// The relevant user will be discovered during the completion stage +func (a *Authenticator) StartPasskeyLoginDiscovery() (*protocol.CredentialAssertion, string, error) { + panic("not implemented") +} + +// Complete a passkey login request for an unknown user. +func (a *Authenticator) CompletePasskeyLoginDiscovery( + sessionId string, + response *http.Request, +) (accessToken string, err error) { + // Get user in question + // Get latest login token data + loginToken, err := dbgen.LoginProcessToken.Where(dbgen.LoginProcessToken.Token.Eq(sessionId)). + First() + if err != nil { + return "", other.Error( + "auth", + "failed to get user's login token for passkey login completion", + err, + ) + } + // Check if that token has expired + if loginToken.ExpiresAt.Before(time.Now()) { + return "", ErrProcessTimeout + } + var pkeySession webauthn.SessionData + err = json.Unmarshal([]byte(loginToken.Name), &pkeySession) + if err != nil { + return "", other.Error("auth", "failed to unmarshal passkey session for user", err) + } + discoverer := a.getPasskeyDiscoverer() + // Hand data to webauthn for completion + newSession, err := a.webauthn.FinishDiscoverableLogin( + discoverer.userWithPasskeyDiscoverer, + pkeySession, + response, + ) + if err != nil { + return "", other.Error("auth", "passkey completion failed", err) + } + // TODO: Utilise clone warning + // newSession.Authenticator.CloneWarning + + jsonSessionId, err := json.Marshal(newSession.ID) + if err != nil { + return "", other.Error("auth", "failed to marshal session", err) + } + jsonSession, err := json.Marshal(newSession.ID) + if err != nil { + return "", other.Error("auth", "failed to marshal session", err) + } + // Update credentials + // WARN: I am not sure if this will work + // Using the ID of the passkey session *should* be unique enough to identify the correct one + // Of course, even then, there's still the problem of matching as + // I can't yet guarantee that the parsed json content for the ID would be the same + _, err = dbgen.UserAuthMethod.Where(dbgen.UserAuthMethod.Token.Like("%"+string(jsonSessionId)+"%")). + Update(dbgen.UserAuthMethod.Token, jsonSession) + if err != nil { + return "", other.Error("auth", "failed to update credentials", err) + } + dbAccessToken := models.AccessToken{ + User: *discoverer.FoundUser, + UserId: discoverer.FoundUser.ID, + ExpiresAt: calcAccessExpirationTimestamp(), + } + err = dbgen.AccessToken.Omit(dbgen.AccessToken.Token).Create(&dbAccessToken) + if err != nil { + return "", other.Error("auth", "failed to generate access token", err) + } + + return dbAccessToken.Token, nil +} + // Start the process of registrating a passkey to an account func (a *Authenticator) StartPasskeyRegistration( username string, @@ -219,3 +302,22 @@ func (a *Authenticator) CompletePasskeyRegistration( } return nil } + +// Get a new passkey discoverer. +func (a *Authenticator) getPasskeyDiscoverer() *passkeyDiscoverer { + // nothing special yet, might use session id for further verification + return &passkeyDiscoverer{} +} + +// userWithPasskeyDiscoverer implements webauthn.DiscoverableUserHandler +// for the use during a discovering login process +func (d *passkeyDiscoverer) userWithPasskeyDiscoverer( + rawID, userHandle []byte, +) (user webauthn.User, err error) { + dbUser, err := dbgen.User.Where(dbgen.User.PasskeyId.Eq(userHandle)).First() + if err != nil { + return nil, err + } + d.FoundUser = dbUser + return &fakeUser{dbUser}, nil +}