New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Hanko passkeys support #14741
base: main
Are you sure you want to change the base?
feat: Hanko passkeys support #14741
Conversation
@merlindru is attempting to deploy a commit to the cal Team on Vercel. A member of the Team first needs to authorize it. |
Thank you for following the naming conventions! 🙏 Feel free to join our discord and post your PR link. |
New and removed dependencies detected. Learn more about Socket for GitHub ↗︎
🚮 Removed packages: npm/superjson@1.9.1 |
.env.example
Outdated
@@ -116,15 +116,31 @@ NEXT_PUBLIC_FRESHCHAT_HOST= | |||
# @see https://support.google.com/cloud/answer/6158849#public-and-internal&zippy=%2Cpublic-and-internal-applications | |||
GOOGLE_LOGIN_ENABLED=false | |||
|
|||
# - GOOGLE CALENDAR/MEET/LOGIN | |||
# Hanko Passkeys Config |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lets mention this lower in the file
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah was unsure about that. wanted to place it below google auth stuff since i did that everywhere else, but agreed, for the env file its a little too high up
We'd really like to use the passkey icon (not part of lucide, it's an SVG) however the components that made use of it (Button, EmptyPage, ...) have all been changed to only accept a lucide icon name (string) Is there any way we still could use a custom component as an icon for these? |
Graphite Automations"Add foundation team as reviewer" took an action on this PR • (04/25/24)1 reviewer was added to this PR based on Keith Williams's automation. "Add consumer team as reviewer" took an action on this PR • (04/25/24)1 reviewer was added to this PR based on Keith Williams's automation. |
While PASSKEY_LOGIN_ENABLED hides all passkey related UI on the login page, the settings page as well as the API routes to register and remove passkeys stay enabled These aren't strictly related to logging in with Passkeys; do you feel like the env var should disable everything related to passkeys or just the ability to login? |
@@ -62,6 +63,7 @@ | |||
"@stripe/react-stripe-js": "^1.10.0", | |||
"@stripe/stripe-js": "^1.35.0", | |||
"@tanstack/react-query": "^5.17.15", | |||
"@teamhanko/passkeys-next-auth-provider": "^0.2.7", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wish Node had a better way to make optional dependencies 🤷♂️
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure since when npm has this, but there is optionalDependencies
https://docs.npmjs.com/cli/v10/configuring-npm/package-json#optionaldependencies
Was supported in yarn v1 so I assume v2+ has it too
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Opted not to do this for now because optionalDependencies isn't in use for any other packages atm. Also I'm unsure if a build without installing optional deps would be successful, especially because of import
behavior, haven't tested it
Do you want me to test this? What other not-strictly-needed dependencies are there that could be moved to optionalDependencies
(at least in theory)?
|
||
export default async function handler(req: NextApiRequest, res: NextApiResponse) { | ||
if (!tenantId || !apiKey) { | ||
return res.status(503).json({ message: "Passkey API not configured" }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is 503 the best status code to use here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Other routes use 403: https://github.com/calcom/cal.com/blob/main/apps/web/pages/api/auth/signup.ts#L23
However, MDN says
[...] 503 Service Unavailable server error response code indicates that the server is not ready to handle the request.
Common causes are a server that is down for maintenance or that is overloaded. This response should be used for temporary conditions [...]
While in my mind 403 has always been more for insufficient permissions etc
What do you think? Maybe 403 for consistency's sake?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd prefer 403 Forbidden, 503 implies a server issue but most of the time this'll be a client issue (don't access the route)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't think a 403 is correct here though because that leads the client to believe they are lacking permissions when in reality the functionality isn't supported at all due to a lack of server configuration. Would consider 501 Not Implemented or 405 Method Not Allowed instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If everyone is fine with it, will go with 501 - seems like the most appropriate IMO
apps/web/pages/auth/login.tsx
Outdated
onClick={async (e) => { | ||
e.preventDefault(); | ||
await signInWithPasskey({ | ||
tenantId: process.env.NEXT_PUBLIC_HANKO_PASSKEYS_TENANT_ID!, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Forbidden non-null assertion needs an eslint comment to exclude or check to prevent
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll check for it explicitly
lastUsedAt?: string; | ||
} | ||
|
||
const PasskeyListItem = (props: Passkey & { onRemoved?: () => any }) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Try to avoid using any
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Crap, I resolved this ☝️ but missed an onError: (e: any) => {}
in another place. Sorry - I'll wait for more feedback before fixing this (so I don't make too many commits)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While PASSKEY_LOGIN_ENABLED hides all passkey related UI on the login page, the settings page as well as the API routes to register and remove passkeys stay enabled
These aren't strictly related to logging in with Passkeys; do you feel like the env var should disable everything related to passkeys or just the ability to login?
In line with current practises it's not needed to fully "unregister" routes
We'd really like to use the passkey icon (not part of lucide, it's an SVG)
Feel free to modify the button component to allow StartIcon's that aren't strings, ideally in a way it doesn't accept lucide icons (strings should be used instead).
const IS_PASSKEY_LOGIN_ENABLED = !!( | ||
process.env.PASSKEY_LOGIN_ENABLED === "true" && | ||
process.env.NEXT_PUBLIC_HANKO_PASSKEYS_TENANT_ID && | ||
process.env.HANKO_PASSKEYS_API_KEY | ||
); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we already have this in constants - lets put it from there
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We do, but the GOOGLE
variables are also duplicated in next-auth-options, so I did the same to stay consistent. Do you know if there's any reason for this?
Otherwise we could import both from constants.ts.
(I don't think the google constants differ from the ones in next-aut-options.ts in any way)
Note: This previously was #13127 but has since been reworked and simplified
What does this PR do?
Fixes #13342
Fixes CAL-2984
This PR adds passkey support for logging in through Hanko Passkeys.
Type of change
How should this be tested?
set it to
HANKO_PASSKEYS_API_KEY="<api key>"
(quotes important)
NEXT_PUBLIC_HANKO_PASSKEYS_TENANT_ID=<tenant id>
Mandatory Tasks
next-auth-options.ts
This file now includes a PasskeyProvider.
For its
authorize
function, I've collected all of the important checks I could find in the email+password provider (a.k.a. CredentialsProvider), such as rate-limiting, user.locked, belongsToActiveTeams, ...) and included them in the PasskeyProvider.For some of these, the fields/relations returned by
UserRepository.findById
wasn't enough. Unfortunately.findTeamsByUserId
doesn't return enough info eitherI didn't want to touch any of the existing queries, so I've added
UserRepository.findByIdAndIncludeProfilesAndPassword
...which is the exact same as the email version of this function (
findByEmailAndInclude...
). It just looks the user up by their ID instead.Checklist
UI
Note about images
These are the same images from the old PR. There's one minor change since then: since icon components aren't supported for
Button
,EmptyPage
, ... anymore, I used Lucide'skey-round
iconThis is what key-round looks like:
Versus the passkey icon:
Adds passkey button to login page:
Adds "Passkeys" settings section to sidebar:
Adds passkeys settings page
Empty:
Clicking on the "add" button and "sign in with a passkey" buttons opens a browser dialog. This looks different for every browser, phone, etc.
For example:
With passkeys added:
Dropdown stolen straight from API settings page: