Skip to content

Implementing passkey authentication#106

Closed
ilblackdragon wants to merge 1 commit into
mainfrom
add-passkey
Closed

Implementing passkey authentication#106
ilblackdragon wants to merge 1 commit into
mainfrom
add-passkey

Conversation

@ilblackdragon

Copy link
Copy Markdown
Member

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @ilblackdragon, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the application's authentication system by integrating passkey (WebAuthn) support. This allows users to log in using modern, secure, and passwordless methods, improving both security and user experience. The changes span across the API, service, and database layers, introducing new data models, business logic, and API endpoints to handle the full lifecycle of passkey authentication.

Highlights

  • Passkey Authentication Integration: Introduced comprehensive support for passkey (WebAuthn) authentication, enabling secure, passwordless login flows for users.
  • New Database Schema: Added passkeys and passkey_challenges tables to the database to persistently store passkey credentials and manage authentication challenges.
  • API Endpoints for Passkeys: Implemented new API routes /v1/auth/passkey/options (GET) and /v1/auth/passkey/verify (POST) to facilitate passkey assertion (login) processes.
  • Dedicated Passkey Service Layer: Created a PasskeyServiceImpl and PasskeyService trait within the services crate to encapsulate passkey business logic, interacting with the new PasskeyRepository.
  • Dependency Updates: Incorporated several new Rust crates, including ciborium, p256, ecdsa, ed25519-dalek, base64, and zeroize, to provide the cryptographic and WebAuthn-specific functionalities required for passkeys.
  • Passkey Configuration: Added a PasskeyConfig structure to manage passkey-related settings, such as the relying party origin, ensuring proper environment-specific setup.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces passkey (WebAuthn) authentication, a significant security and usability enhancement. The implementation is comprehensive, covering database schema changes, a new repository, a passkey service for the core logic, and API route handlers. The code is well-structured, particularly the separation of concerns between the service, repository, and WebAuthn utility modules. My review includes a critical fix for user metrics, suggestions for code deduplication, and improvements for maintainability by removing dead code and magic numbers.

Comment on lines +799 to +805
let is_new_user = passkey.last_used_at.is_none();
let auth_method = AuthMethod::Passkey;
let metric_name = if is_new_user {
METRIC_USER_SIGNUP
} else {
METRIC_USER_LOGIN
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current logic for determining is_new_user is flawed. It checks if passkey.last_used_at.is_none(), which is true the first time any passkey is used, not just for a new user. This will lead to existing users being marked as new when they use a newly added passkey, skewing your signup metrics.

Since this endpoint is for logging in with an existing passkey and does not handle user creation, is_new_user should always be false. The user registration flow should be responsible for setting this flag correctly upon initial signup.

    // This endpoint is for passkey login, not registration.
    // The `is_new_user` flag should be determined by the registration flow.
    // For login, it's always false.
    let is_new_user = false;
    let auth_method = AuthMethod::Passkey;
    let metric_name = METRIC_USER_LOGIN;

Comment on lines +778 to +831
let session = app_state
.session_repository
.create_session(passkey.user_id)
.await
.map_err(|e| {
tracing::error!(
"Failed to create session for passkey user {}: {}",
passkey.user_id,
e
);
ApiError::internal_server_error("Failed to create session")
})?;

let token = session.token.ok_or_else(|| {
tracing::error!(
"Passkey session token missing for session_id={}",
session.session_id
);
ApiError::internal_server_error("Failed to create session")
})?;

let is_new_user = passkey.last_used_at.is_none();
let auth_method = AuthMethod::Passkey;
let metric_name = if is_new_user {
METRIC_USER_SIGNUP
} else {
METRIC_USER_LOGIN
};
let tags = [
format!("{}:{}", TAG_AUTH_METHOD, auth_method.as_str()),
format!("{}:{}", TAG_IS_NEW_USER, is_new_user),
];
let tag_refs: Vec<&str> = tags.iter().map(|s| s.as_str()).collect();
app_state
.metrics_service
.record_count(metric_name, 1, &tag_refs);

let activity_type = if is_new_user {
ActivityType::Signup
} else {
ActivityType::Login
};
if let Err(e) = app_state
.analytics_service
.record_activity(RecordActivityRequest {
user_id: passkey.user_id,
activity_type,
auth_method: Some(auth_method),
metadata: None,
})
.await
{
tracing::warn!("Failed to record passkey analytics event: {}", e);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There's significant code duplication between this passkey_verify function and the near_auth function for handling session creation, metrics, and analytics. This repeated logic makes the code harder to maintain.

Consider extracting this common logic into a helper function. For example, a function with a signature like async fn finalize_login(app_state: &AppState, user_id: UserId, is_new_user: bool, auth_method: AuthMethod) -> Result<Json<NearAuthResponse>, ApiError> could encapsulate these steps, improving code reuse and clarity.

Comment on lines +65 to +71
pub fn generate_challenge() -> String {
let mut rng = rand::rng();
std::iter::repeat_with(|| rng.sample(Alphanumeric))
.take(64)
.map(char::from)
.collect()
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This generate_challenge function appears to be unused. The actual challenge generation is handled in passkey_service.rs which correctly generates random bytes for the challenge.

This function is also misleading as it generates an alphanumeric string, which is not the format used for WebAuthn challenges. To avoid confusion and remove dead code, this function should be deleted.

Comment on lines +136 to +142
fn map_algorithm(&self, alg: i32) -> Result<WebAuthnAlgorithm> {
match alg {
-7 => Ok(WebAuthnAlgorithm::Es256),
-8 => Ok(WebAuthnAlgorithm::EdDsa),
other => Err(anyhow!("Unsupported algorithm {other}")),
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This function uses magic numbers (-7, -8) to identify COSE algorithms. To improve readability and maintainability, you should use the constants (COSE_ALG_ES256, COSE_ALG_EDDSA) defined in crates/services/src/auth/webauthn.rs.

    fn map_algorithm(&self, alg: i32) -> Result<WebAuthnAlgorithm> {
        use super::webauthn::{COSE_ALG_EDDSA, COSE_ALG_ES256};

        match alg as i8 {
            COSE_ALG_ES256 => Ok(WebAuthnAlgorithm::Es256),
            COSE_ALG_EDDSA => Ok(WebAuthnAlgorithm::EdDsa),
            other => Err(anyhow!("Unsupported algorithm {other}")),
        }
    }

@hanakannzashi

Copy link
Copy Markdown
Member

See #131

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants