Passkeys are the new standard login method for web authentication but so far there are very few developer tutorials that show how to add them on a code level.
In the following, we will change the password-based login of our PHP Symfony app to a passwordless, passkey-first authentication with Corbado's web component.
The web component is an easy to integrate, lightweight authentication layer with great out-of-the-box passkey UX.
Complete tutorial for the integration of the Corbado web component for passkey-first authentication using PHP Symfony.
Complete PHP Symfony integration example for Corbado's passkey-first web component
⚠️This repository is no longer maintained. Please have a look at our new PHP Symfony passkeys repository, which is the successor of this repository.
This is a sample implementation of frontend and backend where the Corbado web component is integrated.
Note: In this tutorial a customer system is created with some pre-existing password-based users. Have a look at our docs to see the integration if you don't have any users yet.
1. File structure
├──
├── config
| └── routes.yaml # Assigns paths to controller methods
├── src
│ ├── Controller
│ │ ├── WebhookController.php # Manages endpoints for webhook
│ └── └── AppController.php # Manages endpoints for application
├── templates
│ ├── home.html.twig # Home page which you only get to see if you are logged in
│ ├── login.html.twig # Login page which contains
Let's get the whole picture first! We use a dockerized setup with three containers:
The first container runs our PHP Symfony application
The second container hosts the database with our existing users.
The third container runs phpMyAdmin to provide easy access to the database
Our folder structure looks like this
├── ...
├── config
| └── services.yaml # Service configurations
├── src
│ ├── Controller
│ │ ├── WebhookController.php # Manages endpoints for webhook
│ └── └── AppController.php # Manages endpoints for application
├── templates
│ ├── home.html.twig # Home page which you only get to see if you are logged in
│ ├── login.html.twig # Login page which contains the Corbado web component; Acts as landing page if you are not logged in
├── .env # Contains all Symfony environment variables
└── ...
We start by heading over to the Corbado developer panel and create an account. We are welcomed by this screen
We click on 'Integration guide' to do everything step by step.
We want to use the 'Web component', so that's what we select.
Also, we have a system with an existing user base, so we click 'Yes'. Afterwards, we find ourselves at the overview of the developer panel (you need to confirm your account via email if it's your first time).
Head over to the 'Integration guide'. This guides us through all the steps necessary for the integration to work.
In step 1, we create an API secret. We need the project ID and the generated API secret later in order to communicate with Corbado's API. Also, we need to configure our authorized origin. The authorized origin is the browser URL from the site where the web component is integrated (with protocol and port but without path). We need it for CORS (cross-origin request sharing) and binding passkeys to it. In our case, we send it to http://localhost:8000.
In the second, optional step, we configure the webhook. This is needed, so Corbado can communicate with our backend e.g. for checking if a username and password of an existing user match. More details on that later.
We will later set up our webhook at http://localhost:8000/corbado-webhook with ‘p36qLbyRvHoZDT’ and ‘z9aSidcu2xLyoU’ as credentials, so we can already enter that here.
In step 3, we add our Application URL and Redirect URL. The Application URL is the URL in the frontend where the web component runs. For example, it’s used to forward users to the web component again after they clicked on an email magic link.
The Redirect URL is the URL that receives Corbado’s session token as GET parameter, after Corbado has successfully authenticated a user, so that a session can be started. We implement our Redirect URL at http://localhost:8000/redirect later, but we can enter it here already.
The Relying Party ID is the domain where we bind our passkeys to. The domain in the browser where a passkey is used must be a matching domain to the Relying Party ID. As we test locally, we set the Relying Party ID to localhost.
Just like that, the project is set up!
2. Integrate the Corbado web component in the frontend
Now that we are done with the Corbado settings, let’s get to coding! In the frontend, we integrate the Corbado web component into our login page. Add the following, code in the HTML head, so it loads the JavaScript in advance:
Taking a glance at our login page in the browser reveals the Corbado web component in action:
Hint: If you now enter your email address and tried to sign up or login, you will get an error message. That's because we haven't configured the backend yet.
3. Create backend endpoint to generate a session
Once a user has successfully authenticated via the Corbado web component in the frontend, he gets sent to the Redirect URL with a corbadoSessionToken GET parameter. We configured this Redirect URL to be http://localhost:8000/redirect, remember? We implement this endpoint now.
We’ll initialize the Corbado API client inside the services.yaml file. For this we need the project id and API secret from above.
In the redirect endpoint, we read the value of the corbadoSessionToken query parameter. Using the Corbado PHP SDK, we then call Corbado’s sessionVerify API endpoint which will give us the email and name of the user which the token belongs to.
Publicfunctionredirect(UserRepository$userRepo,Request$request,SessionInterface$session,Client$apiClient):Response{$token=$request->query->get(‘corbadoSessionToken’);$useragent=$request->headers->get(‘User-Agent’);$remoteAddress=$request->server->get(‘REMOTE_ADDR’);try{$result=$apiClient->widget()->sessionVerify($token,$remoteAddress,$useragent);}catch(\Exception$e){returnnewResponse(‘Sessiontokenexpired’,400);}$response=$result->getData()->getUserData();// Parse json response from Corbado request$userData=json_decode($response,true);$username=$userData[‘username’];$userFullName=$userData[‘userFullName’];// Create user if not exists$user=$userRepo->findOneBy([‘email’=>$username]);if($user===null){$user=newUser($userFullName,$username);$userRepo->save($user,true);}$session->set(“userID”,$user->getId());// Forward the user to frontend pagereturn$this->redirectToRoute(‘home’);}
We now know who just logged into our application. We create a session for the user and redirect him to a logged-in page of the app:
If you don't have an existing user base, that's all it takes to integrate passwordless, passkey-first authentication with Corbado!
4. (Optional) Enable password-based login as fallback
We don’t want to lock out our existing users, so we enable password-based authentication as a fallback. Therefore, Corbado needs to communicate with our backend. It does so via a webhook that is set up in our backend. In the steps before, we set the webhook URL to ‘http://localhost:8000/corbado-webhook’ as well as the webhook username and password.
Corbado provides some PHP code that helps to add the webhooks:
composer require corbado/webhook-php
Then, we add the username and password of the webhook to our PHP Symfony environment variables, so that they are available when we want to authenticate an incoming webhook call:
as well as a controller to make the webhook available.
The webhook controller uses the previously installed Corbado webhook library and the route we protected via HTTP basic auth.
classWebhookControllerextendsAbstractController{#[Route('/corbado-webhook', name: 'corbado', methods: 'POST')]publicfunctioncorbadoWebhook(UserRepository$userRepo,Request$request,string$webhookUsername,string$webhookPassword):Response{try{// Create new webhook instance with username and password. Both must be// set in the developer panel (https://app.corbado.com) and are used to secure your// webhook (this one here) with basic authentication.$webhook=newWebhook($webhookUsername,$webhookPassword);// Handle authentication so your webhook is secured (basic authentication). If username// and/or password are invalid handleAuthentication() will send HTTP status code// 401 (Unauthorized) and terminate/exit execution here. $webhook->handleAuthentication();
If a user enters their email in the web component, the webhook we just installed gets a call. The corbado_webhook function will handle it resulting in a call of the userStatus function. If the user exists, meaning he already has a password, Corbado gives him the option to login via password.
functionuserStatus(UserRepository$userRepo,string$username):string{$user=$userRepo->findOneBy(['email'=>$username]);//Look up in database if $username exists and if $username is blocked/not permitted to loginif($user==null){returnAuthMethodsDataResponse::USER_NOT_EXISTS;}if($user->isBlocked()){returnAuthMethodsDataResponse::USER_BLOCKED;}returnAuthMethodsDataResponse::USER_EXISTS;}
Once the user has entered his password, another webhook call is issued resulting in a call to the verifyPassword function. There, we check our database if the given credentials match and send the response back.
Done! All our users can now use passkeys, while existing ones have their password as a fallback option.
But wait, how can Corbado call endpoints if our backend is only running locally, especially during development?
5. Make your local application reachable for Corbado via Corbado CLI
Using the Corbado CLI, we create a tunnel of our local application to the outside world, so that the webhook URL can be called. We install it by following the docs
Once started, the webhook running on our machine forwards requests from Corbado to our local instance. Analog to the image above, the process is then as follows:
The browser requests our login page
The AppController sends back the HTML page which contains the web component.
After the user has entered the email address, the web component sends it to Corbado.
Corbado processes it and sends a request to our webhook to check if the user exists.
The message gets forwarded through Corbado CLI in order to reach our local instance. (If your solution is in production, Corbado can directly call the Webhook controller without the need for the Corbado CLI tunnel because you then have a public address).
Our webhook controller sends back the requested information.
The message gets forwarded through Corbado CLI again
Corbado tells the web component how to react (e.g., ask the user for a password).
If we now go to http://localhost:8000/login and enter the email of an already existing account, the web component should ask you for a password (if no passkey for the account has been created yet).
Afterwards, users are asked to create a passkey for future logins.