Adding PKCE to a Magic Link flow

Proof Key for Code Exchange (PKCE) is a specification built on OAuth 2.0 that was introduced to create a more secure login flow. We've adapted PKCE to work on magic link flows as well - increasing their security while preserving their usability.

You can read more about PKCE's role in OAuth in our PKCE OAuth guide.

PKCE makes magic links more secure by cryptographically validating that the login flow starts and ends on the same device. In a standard magic link flow, the link contains a one time code in a redirect URL. The user clicks on the link, the app receives the code, and the code is exchanged for a session to log in the user. This setup means that an attacker would be able to impersonate a user if the attacker gained access to the one-time code. Codes in a URL can be leaked in many different ways - either through improper logging, HTTP Referrer headers, or even through another exploit like XSS.

PKCE improves on this by introducing a one-time secret for each authorization flow. This secret, called a code_verifier, is generated and stored by the app on the user's device. The verifier then is hashed to produce a code_challenge. This challenge is passed along with the user's email to Stytch when the user is asked to verify their identity.

Stytch sends the user an email containing a magic link. When the user clicks on that link, the user is redirected back to the app with a secure code. Next, the app submits the code along with the original code_verifier. Stytch hashes the submitted code_verifier and validates that it matches the code_challenge passed as a query parameter earlier. This check ensures that the token has not been leaked, and that it is being handled by the same user that was seen at the start of the flow.

This guide will show you how to add PKCE support to your Stytch magic link integration for added security. A PKCE integrations is required for applications that use native callback URLs, for example appname://some/callback on mobile. Stytch's support for PKCE differs slightly from the original specification - for example we do not permit plain verifiers to be used - but the original idea of strengthening app security with a unique secret on every request remains the same.

If you get stuck while working through this guide, feel free to ask questions in our forum, via support@stytch.com, or in our Slack community.


Choose integration method

JavaScript SDK

Step 1: Confirm PKCE makes sense for your application

PKCE provides cryptographic assurance that a login flow starts and ends on the same device. While this is almost always a desired property, there is one notable exception to the rule: mobile webviews. Consider the following scenario:

  1. A user sees a link to your application embedded in another mobile application, such as Twitter or Slack.
  2. The user clicks the link and the application is opened inside a webview owned by the parent application.
  3. The user is logged out by default, because cookies and session state are not shared between webviews and the native mobile browser.
  4. The user attempts to log in via an email magic link. The PKCE code_verifier is generated, and stored inside the webview.
  5. The user leaves the initial webview and opens their email application. They click the magic link in the email, and your application is opened again, this time in the native mobile browser.
  6. Your application attempts to submit the magic link token, but does not have access to the original code_verifier. The user fails to log in with a pkce_expected_code_verifier error, and needs to request a new magic link from the native mobile browser. Depending on your application's userbase, mobile webviews could make up for a significant amount of traffic, and PKCE would add friction to that user experience. For cases like these, you might choose to omit PKCE and supplement your login flow with a second factor like SMS or WebAuthn instead.

If your application caters to desktop users, native apps, or is not shared on social media, you can implement PKCE with confidence.

First, enable PKCE for the JavaScript SDK. In order to do so, toggle the option in the SDK Configuration page of the Stytch dashboard. Once this configuration is set, the SDK will automatically start applying PKCE to all magic link login flows it creates.

Call the magicLinks.email.loginOrCreate() method to start the magic link flow. The SDK will generate a code_verifier and store it in local storage before asking Stytch to send the email. If you use the SDK's built in Magic Link UI this step is already handled for you.

function SendMagicLink({ email }) {
  const stytch = useStytch();
  const startMagicLinkFlow = useCallback(
    () => stytch.magicLinks.email.loginOrCreate(email), 
    [stytch]);

  return (
    <button onClick={startMagicLinkFlow}>
      Click here to send a magic link to {email}
    </button>
  );   
}

At the route that the user is redirected to after the Magic Link flow, pull the token out of the URL params and call magicLinks.authenticate(token) to exchange the token for a Stytch session. The SDK will automatically retrieve the code_verifier created earlier from local storage and pass it on to the Stytch servers.

function MagicLinkCallback() {
  const stytch = useStytch();

  useEffect(() => {
    const params = new URLSearchParams(window.location.search);
    const token = params.get('token');
    if(!token) {
      // TODO: Redirect back to logged-out view 
    }
    stytch.magicLinks.authenticate(token, {
      session_duration_minutes: 60,
    }).then(() => {
      // TODO: Redirect to final logged-in view
    })
  }, [stytch]);

  return (
    <div>
      Authenticating....
    </div>
  );   
}

Step 5: You're Done!

You just finished all the critical components to authenticate your users via magic links with PKCE! Have any feedback after integrating? Get in touch with us and tell us what you think in our forum, via support@stytch.com, or in our Slack community.