 Hi, I'm Aaron Parecki. In this workshop, we're going to take the to-do application from the Getting Started Guide and make it Enterprise Ready with OpenID Connect. What we're going to do throughout this workshop is let your customers of your to-do app sign into this application with their own OpenID Connect server. Now, I'm going to assume you've already gone through the Getting Started Guide and set up the to-do list app and are able to log into it. If that's not the case yet, please go back and watch that video to make sure you can actually log into the application and browse the database. You should be seeing the login page like this with the username and password, and you should also be able to launch Prisma Studio to browse the database here. We're going to use this throughout the exercise to be able to actually see and modify the records that we're adding for organizations and other user accounts. So in this base application, what we have here is a very simple to-do list app with just email address and password login. Every account is an individual user account. There's no such thing as teams or organizations yet. Every user logs in with their own password. This is a pretty common situation to be in. You've launched your app, you've got regular username and password login. Now what we're going to do is assume that one of your users loves your app so much that they want to get the rest of their company on board using your application. That company happens to be a pretty large enterprise company with an IT department, and their IT department doesn't want the rest of the users at the company to log in with a password at your app. Instead, they want to use their own OpenID Connect server to be able to sign into your application. That means you no longer have to worry about managing passwords for any of these users at that company. That's all handled by the IT admin with their SSO server. Now, thankfully in this case, their server is actually OpenID Connect compliant. And that means there's a standard you can follow to integrate with their server without having to do anything really special for them. So what we're gonna do is we're gonna make it so that instead of an email address and password field on the login page, they actually start with just an email address. The idea here is if they enter an email address that's at that company's domain, then instead of asking for a password here, we're actually gonna redirect them to their OpenID Connect server. If someone enters an email address that's not part of that domain, just one of your regular users, we will then show the password field so that they can continue to log in with just a regular password. One of the other changes we're gonna make to this application is that we're actually gonna add the concept of organizations into this to-do list app. And that means users will belong to orgs and also to-do list items will belong to orgs. Later on you might wanna change this to be able to do things like create shared to-do lists within an organization or let everybody in an org collaborate on to-do items. So you might already be familiar with OpenID Connect. If not, this is not gonna be a deep dive on how OpenID Connect works. I have plenty of other videos online about how OpenID Connect works and you can go watch those if you're curious. Instead, we're gonna assume that we're just going to integrate an OpenID Connect server using a passport JS library. But instead of integrating this like you might normally integrate OpenID Connect with an application, we're actually gonna have to support multiple and arbitrary OpenID Connect servers rather than hard coding to one in particular. So let's jump into the database and start with a couple of database changes that we're gonna need to make. In this application, the schema is actually defined in a file called schema.prisma. This is a syntax for defining tables and then it will actually go and create the SQLite database for you based on what you've got in this file. So this is our existing schema. We've got a pretty simple relationship between to-do items and users. We can see that to-do items belong to a user and the user has multiple to-do items. We're now actually gonna add another table, which is an organization table. We're then gonna also associate users and to-do list items with that organization. So let's go ahead and actually do that really quick. I'm just gonna go ahead and make a new model here called org. We do want it to have an auto increment ID, although in reality you probably don't want to use sequential numbers for this. But for now, this is fine. The main identifier for this organization will actually be the email domain. So we're gonna make a string field. There's a couple other properties we need in order to actually do an OpenID Connect flow. And this is again one of the unique things about supporting this sort of multi-tenant environment. We actually have to store the organization's OpenID configuration for each org rather than putting into an environment variable or hard coding into your app. So we're actually gonna put this organization's OpenID configuration into the database here. So we're gonna make a bunch of columns that are all specific to OpenID Connect. So here I've just added five columns. The authorization endpoint is the URL that we're gonna redirect the user to to go log in. The token endpoint is where we can get the ID token from from the OpenID Connect server. The user info endpoint is where we can get the user's profile info like their email address or their name or other information. Client ID and secret is this application's credentials with this OpenID Connect server. Later on, we might also want to be able to provide an API for your application, your to-do list application for this organization. So we're just gonna add an API key column here as well. And then we can set the relationships between to-dos and users. So I'm gonna add, so I'm just gonna go ahead and copy this from the user here. And we'll add another one for user. We also need to define the reverse relationship. So in the to-do table, we're gonna go ahead and add the back link to the org. So the org will be here and it is going to, I can just copy the relationship from up here and change it. Relation is org ID and references ID. And then we're gonna add that column as well. So this is adding the org ID column to this table. And then we're gonna copy these and do the same thing for users. So one of the other changes we have to make now is actually how we're storing users. We actually no longer want to treat the email column as the unique column for users, unique identifier for users. We're gonna take out this unique constraint. This is a little bit weird, but instead what we're gonna do is we're actually gonna add a new column, which is the, we're gonna call it external ID. And this is actually just a string, which is going to be the string identifier of this user at their organization's OpenID Connect server. Let me back up a second. We've got a whole bunch of users in this database. And I've got my own local IDs for these users, right? But as soon as somebody logs in from an external OpenID Connect server, that server also has its own unique ID for users. It turns out email addresses are actually not a good idea to use as unique identifiers, especially once you're starting to use these kind of federated login systems. There's a couple of really concrete reasons for this and there's a couple of more subtle reasons. But the really obvious reasons are that a user's email address might actually change. For example, if they change their name, their company might give them a new email address. Or if the company's acquired, the entire company domain might change. So you don't want to actually rely on external email addresses as unique identifiers anymore. And that's why in the OpenID integration, there's actually a separate field called Subject, and that is a guaranteed to be unique and stable identifier for that user at that server. Now, that's not globally unique, so we can't treat it as a globally unique identifier, but we can treat it as a unique identifier at that server. So back in our schema, we actually can go make a new unique constraint. I can spell unique correctly, which is going to be a combination of org ID and the external ID. This means the unique constraint for users is now, at that organization, that external ID. So if somehow there happened to be the same external ID at two different OpenID Connect servers, we're still going to see it as two different users because we're also mapping it to the org ID. I also just realized we actually need one more column in the org table, which is the issuer. The issuer is actually the actual identifier of the OpenID Connect server, and this is important in order to be able to retrieve the configuration, but it's also to identify tokens to make sure they're coming from the right place. I'm just going to also go ahead and make this a string with the default value blank. Okay, so we are ready to go ahead and create the migration and migrate the database. Let's go ahead and do that, pop over to the terminal. We'll close out of the Prisma Studio and we'll run npx Prisma generate, and that'll generate the new schema. To actually run the migration, we're going to do npx Prisma DB push. I made a mistake here. This is saying that the required column org ID on the user table does not have a default value, so it's going to want to reset everything. Let's back out of that and actually fix this. I forgot that this actually needs to be an optional field, so the question mark makes this column optional and it'll default to null that way. Let's double check if that works. Try to generate the schema again and push the migration. Okay, great, that worked. So we can now run npx Prisma Studio. Let's take a look around the database and see what we've got now. So now users can belong to an org. There's an org ID column and there's also an external ID column. Our two users that we have right now are not part of an organization yet, which makes sense because we just applied that migration. The to-dos can now also belong to an organization and now we also have another database in the table called org. Obviously nothing's in here because we just created that. So before we get too much farther with the database, let's start writing some code. So we're going to start with changes to the front end only. Remember this application is a single-page app front end and it has an API that the front end uses to talk to. We're going to start in the front end. So for this, what we want to do is actually hide the password form and then we're going to make the email address actually first submit it to the back end to check if that user belongs to an organization and if it does, we're not going to show the password field and we're going to start an opening connect flow. If the user does not belong to an organization, we'll then show the password field. I also want to remind you that the completed version of this project is on GitHub. So you don't need to worry about copying what I'm doing on the screen. You can actually see the completed version by checking out that branch from the GitHub repo. So we're going to go start breaking into the code. First things first, we need to just hide that password form. So let's go into the to-do app. This is the front end file and we're just going to open up the sign in.tsx file. This is where we've got the two fields, email and password. Now this is a React application, so we'll keep that in mind. So we're going to take this whole password thing down here and we're actually going to need to be able to target this in our JavaScript. So we're going to give it an ID. We'll call it password field and then we can just add the attribute hidden here and that will hide the field. Let's save that and go back to the app. Make sure we're running the app. Okay, the app is running. The password field is hidden. Now we need to actually add a hook, which is when they actually enter their email address. If they enter their email address and click sign in, we need to do something that's not attempting to log in with the empty password, of course. So we're going to now change when this submit button is disabled by just removing the check for password. So we're going to enable it as soon as they enter a username. Go ahead and save that and see what happens now. So can't click it right now, but if I enter something in the email address field, now it's clickable. So now we need to write a JavaScript function to actually handle this and when they click sign in, do something different depending on whether there's a password field filled in. Let's go ahead and just scroll up a little bit and we're going to add a new function into here. So right now there's just the one event which is when they click log in, it's going to go and try to actually log in. We're going to make a second one which is just if a username is entered. So that's actually going to have to be inside of this component, this auth state component. So we're going to go ahead and open that up first. So there's a couple of places we need to change in order to actually make this new exportable function. And what we're going to do is just copy the user is authenticated function and just make a new one called user on username entered function. So I can just copy that. It's actually going to look more like the authenticate on authenticate function. So we're going to go and copy that. We're going to go through this, call it on username entered function and they're going to enter a username but no password. So it's like that. And we're actually going to say it's going to return a number or null. The reason for that is because what this is going to do, this is going to send their email address to the backend. The backend is going to say this email address belongs to an organization. Here's the org ID or this does not belong to an organization. So the return null. So that's that bit. And then let's copy the default auth context. So this is going to be on username entered function is going to return null. Now let's write the actual function for this. Oh, actually one more thing. Let's go on to the bottom and just add it to the list of things that are exported. Here's our user authenticated function. So we'll add, or sorry, on authenticate function on username entered function. So now we're exporting that as well. Great. Now we have to actually write the function. So let's go ahead and copy the shell of this one and we'll call it on username entered function. And again, no password. So we obviously don't have the backend done yet but we're going to write that later. Let's go ahead and just write the front end code pretending the backend code does exist. So just like this API sign-in, we're going to have a new endpoint in the backend which will be, let's call it open ID check. And that's the URL that we're going to have to create in a little bit. And what that's going to do, again, it's going to look similar to this. So I'm just going to copy this to be able to speed things up. We are going to fetch that URL. It is going to be a post. It will be adjacent. And instead of username and password, it's going to be just username. So we're sending just their username or their email address to the backend. The backend's not going to return a name. It's going to return an org ID. The org ID will be either a number or null. So we're not going to, this is like actually getting them logged in on the front and we're not going to do that. We're going to instead just return the org ID. Great. And if anything else happens bad, then we'll throw the error in the console. But if there is no org ID or if there's some other problem, then we're just going to add a return null here and that will let the front end continue on normally by showing the password field. So this gives us that on username, entered function. Now we go back to the sign in page itself and now we can use it. How do we use it? To pull it in. So we pull in the on authenticate function. We're also going to pull in the on username, entered function. And now we're going to change what happens when they click this button. So on authenticate, that's the function that runs when they click the sign in button down here. So click sign in and that's going to run this function. So what we're going to do is do something different depending on whether that's a password. So let's go ahead and add a new section up here. If there is a username, but there is no password, what are we going to do? We're going to go and try to get that org ID. So we have a function for that now. So we can go ahead and call it. So const org ID is a wait on username, entered function. And we're going to pass it to username. That's going to return either a number or null. If it does return an org ID, then great. Now we're ready to actually start the open ID connect flow. What we're going to do this is actually have the backend start the open ID connect flow because we don't want the front end to have the tokens from the external organization. We want the backend to know who the user is. The front end doesn't really care about any of this. The front end is going to keep using its own sessions to its backend like the original app does with password login. So what we're going to do is actually just tell the front end to redirect to the backend and let the backend deal with open ID connect. So to do that, we're going to do just window.location assign and that's going to tell the front end to redirect the browser. And we're going to redirect the browser to the backend URL with this org ID, which is a new route we're going to have to make. So it's making a list of all the routes we have to create for the backend. I'm going to cheat here and just redirect to local host because I don't want to deal with making a config file so the front end knows the URL of the backend. Obviously that would have to change in production but this is good enough for now. So we're going to make a new route called open ID slash start slash org ID. And I'm actually going to change this to the JavaScript template syntax so I can do that. Great. And return. So this doesn't exist yet in the backend but we're going to create that shortly. And this will start the open ID connect flow for the given organization, which is again something quite different from what you've probably been doing with hard coding open ID connect servers. So if there is an org ID, we're going to go start an org ID C flow. If there is not an org ID, then we're going to show the password form. So this is why we needed to give it an ID, password field and then remove the attribute hidden and return. And then that's going to handle the case of when they entered a username but no password. So we can just add a comment that describes that but no password. Check if the email is part of an org. Great. So we're just going to keep this code down here which is if they enter username and password and that's for our existing users that are not part of an organization so they can continue to log in with the password. So this should be it for the front end. Let's go ahead and save it and see if it's doing what we want. Obviously the backend doesn't exist so it's not going to actually work but we can look at the request to see if it's actually behaving properly. So let's reload, let's enter an email address that doesn't exist and it returned it actually did make that check. We can see it did call that open any connect check and this returned an error obviously because we don't have that route defined but the front end handled it well. The front end said, oh, well, I didn't get back an org ID so I'm going to show the password field. And now if we enter one, two, three, four and try to log in, we should get an error. That returned 401 unauthorized so and we're back to the beginning. I think our front end's done. So let's fill in the rest. So let's now fill in the backend with and actually make this thing work, cancel out of that app. Now in order to actually do the open any connect flow the good news is there is a library for it. So we're already using passport for authentication in this application and there is a passport library for open any connect. So let's go ahead and actually install that. This should just add passport into the project and what we can do is we can see that it actually has updated the package.json and the package.json now includes passport open any connect. We know we need that. Let's close the front end files and go over to the backend. Now in reality, you would probably want to, you know do this in a separate file but we're just going to combine it all into this one file here. So we're just going to make a new section at the bottom for our new routes and I'll just put a big old comment down here. Open any connect routes below. Let's first stub out the routes we need before we actually fill them in. I like to do that just to kind of keep things organized. So we know we're using that first method that the front end calls to check if email is part of an org. So let's go ahead and create that first. So here's the check API open any check we're going to fill this in later. And what it's going to return is actually that org ID or null. So let's stub that in. The other route we need to define here is the route in the front end we'll redirect the user to to actually start the open any connect flow. Now this is not a post request because this is not something that the JavaScript code is actually like calling itself. This is actually going to be a get request because the browser is going to actually visit. So I'm going to go ahead and stub this out again. Also again, one more call out. In practice you probably shouldn't use sequential IDs because that's going to tell everybody how many customers you have which is maybe not something you want to share. So probably just use random IDs in the future but it's easy enough for now we're just going to use sequential auto increments. This is going to actually to do start the OIDC flow. Okay. We'll figure that out in a minute. That'll use passport though. What happens when the open any connect flow starts is the browser is going to hit this route. This is going to start the OIDC flow which will redirect the user to their provider and then they're going to get redirected back to this backend. So we need to make the route that's going to catch that redirect. That's that callback URL or redirect URL. So let's just copy this and call it open ID callback and finish the OIDC flow. So those are the three routes we have to define and that will hook things up. So let's start with the open ID check. We know the front end is sending a parameter call the username. So let's just grab that out of the request. This is going to grab the request body and pull out the parameter call username. Now we've got it as a local variable. So we need to find the domain of the email address. So that's going to be basically just looking for the at sign and then grabbing whatever's after it. Let's write a function for that. So we're going to have to define this function but we'll call it get domain from email. We'll write that a little bit later. If for some reason they entered something that's not an email address, the domain will be null. So it'll just return null or gritty. So if there is a domain, now we have to check do we know about that domain and do we have an organization configured for it? So this goes back to our org database. Remember in the org database, we have a list of domains and then the open ID configuration stuff over here. So there's nothing in here yet but we'll add a record in a minute to actually try this out. So we're going to have to query that table. So now we have to go break open our little Prisma ORM and we're going to say query the org table, find the first record that matches the domain. If it does find one, then cool. We have an organization configured so we can actually return that org ID. So let's say if there is an org and let's make sure that there's actually an issue we're defined on the org which is our open ID connect configuration then we can return org ID is the org ID. This is pretty good but there is another edge case to consider here which we should probably deal with. Let's say for example, there's another user at this company but they have a different domain in their email address for whatever reason. Maybe it was because it was a previous company that got acquired by them and they were already a user. You'll probably end up with some of these weird edge cases and we do want to be able to handle that. So let's go ahead and make another user here but add a different domain. Let's say Leo at black rabbit, not fake. Save that, go ahead and add that record. Now in order to actually demonstrate this I'm actually going to create the org. So we're going to fill in the open ID stuff later but for now let's just add a org record here and the domain will be white rabbit, not fake. Again, we're going to add the open ID configuration a little bit later. So let's go ahead and save that and for now I'm going to just manually set the org ID on Leo to one. Okay, and this is the edge case that you're going to want to handle. Our code is going to auto update any user at the domain that matches the org domain later. We can solve that programmatically but what about users that might have a different email domain that are part of that org? So this way we can actually go ahead and actually assign users to orgs arbitrarily here and this is the case that's not handled in our code yet. So in our code let's say there was no org found for black rabbit, not fake but we do have a user account for them and they do belong to an org. It just didn't match the email domain. So that's the query we have to write now. Let's go ahead and write that query. So that's going to say org await. So this is going to be a kind of complicated SQL query that's written in this ORM language. We're going to look for an org and we're going to look for an org that matches where a user has a matching email. So this is a long way around writing a query that's basically joining the two tables, right? We're taking the org table, the user table, joining them and looking for a matching user record and then we're going to return the org record. And if that's the case, now we've got an org record and if there's an open ID configuration set for it then it will return the org ID. This will let us handle the case of a user that doesn't have a matching email address that does belong to an org, also being able to use that org's open ID configuration. This should be enough to actually see if this works now. So let's go ahead and test this on the command line. Let's run npm start and then open one more window. Oh, we didn't define that function so we can't finish yet, I forgot. Let's go to find that function, get domain from email. This is how I like to do things. I like to assume that things are done watch them break and then go fill them in later. So let's write that function now since we kind of skipped over it. This is just gonna be a simple function and it's gonna take an email address and it's gonna return the domain part after the outside. So let's define a variable to hold it, return that at the end and then here we're going to use a regular expression to parse it. Let's do that return null if there's any kind of error and domain is email.split at the at sign and grab the second part. Don't even need a regular expression, we can just split. Obviously this is not super robust code. You should probably write one that's a little better in reality, but this will get the job done for now. Let's see how our API is doing. Looks like we're not getting an error anymore. So let's test it on the command line. Our API is listening at localhost 3333. So let's curl API open ID check. That's the route we just defined and we have to give it JSON data of username and an email address. We can test an email that doesn't exist. I even spelled the example wrong and that is still the right result. OrgID is null. But if we enter an email address that does belong to an org, which this one does. So we should be able to enter Leo at blackrabba.fake and get back the correct org ID. So let's try that. Oh, right. We haven't actually defined any open ID configuration yet. I'm gonna go ahead and cheat and just put in an issuer here. Example.com, we'll come back to this later. Run this again. So one critical piece I forgot is that you actually have to send the content type header in your request. Otherwise the express framework does not actually parse it as JSON. So I'm gonna go ahead and actually add the content type application JSON header and give it the address Leo at blackrabba.fake which is mapped to this org one. Org1 has a different domain, but it has an issuer. This should return that org. One more test. Let's try it with Trinity at whiterabba.fake who right now is not associated with the org but does have a matching email domain. So let's change the email address and that also gives us back the org ID. And one more try with an email that's not at either domain. We get no org ID. So at this point the front end should now actually attempt to redirect to the route that doesn't do anything yet if it has a matching email otherwise it will show the password form. So let's go ahead and test that on the front end. Here we're gonna say bob at example.com which does not exist. We see our password field. Let's refresh this and enter Trinity at whiterabbit. Click sign in. Doesn't look like it's doing anything, but it is. It is trying to redirect to our back end. Did not show the front end but our back end doesn't do anything yet. So we don't have anything to actually redirect to yet. Okay, so this is a good time to take a quick break. We're gonna come back in a minute and actually then go wire up the rest of this which is going to bring in the actual OpenIDConnect library and do the OpenIDConnect configuration and redirect out, come back and create the user. All right, welcome back. So the next step is to actually wire up the OpenIDConnect library. For this we're gonna be using the passport.js library which we've already installed previously. So it should be available to us now. Let's take a look at the documentation for passport first to see how this is gonna work. Normally when you're using passport, you're gonna do something like this where you're creating an OpenIDConnect strategy with your OpenIDConnect server's configuration as well as a client and client secret. This is where things are gonna have to be a little bit different for our setup here because we don't have a single OpenIDConnect server that we're integrating with. We actually need to create these strategies kind of on the fly for each customer who is going to be using our application. So instead of hard coding these, we have to create this strategy depending on each organization that is configured in the application. Remember the database has a table for organizations and this is where we'll put the OpenIDConfiguration for each org. So each of our customers that has an OpenIDConfiguration integration will get a new record here with new items for the authorization endpoint and client ID and things like that. That means we're gonna have to pull the values out of the database in order to actually create the passport strategy. The other thing to note about the passport strategy is this verify function. This will be called by the library after the flow itself is complete and this will contain information about the user that just authenticated at the OpenIDConnect server. This is where we'll have to customize this. Ours looks a little bit different but we will instead write our own code here to create users in our database. So what I'm gonna do first is go into the code and let's actually just get the OpenIDConnect flow working because this is kind of the easy part because it's just calling the library. If we go back to the documentation for passport, actually using it is really just a matter of calling passport authenticate with a couple of options for how we're gonna handle redirects and errors and things like that. The trick is that instead of hard coding this strategy we have to actually create the strategies dynamically. We're gonna do this part first, create the strategies in a minute. I'm also gonna go ahead and create one more helper function here which is just gonna be used in a couple of our different routes and that is a function that's going to find an organization given the organization ID. Remember how we have this route here OpenID start slash ID the front end is gonna redirect the user to here. This ID will be a record in the database that corresponds to which OpenID configuration we're gonna use. So we need to pull out this record first so that we can actually use these values. If I were to just write this here it would be something like this. We'll find the first org where the ID is the ID parameter from the URL here. This I'm gonna save into a function because we're gonna use this in the other route as well. I'm just gonna cut that out, make a new function up here, org from ID and we will go ahead and change that just to ID and down here now we can use it. So now we can say org is org from ID and give it the parameter. We're gonna use that down here as well. We should probably add a little bit of error handling if there is no org configured like if they enter ID that we don't have configuration for then we'll just return a 404. So we're ready to start the flow but we don't have the strategy created yet. So let's do this. Let's say strategy is create strategy and we're gonna write this function in a minute from the org. And again if this ends up failing for some reason like if there isn't an open ID configuration set up we will then also return a 404. So let's go ahead and copy this down here as well. And now we can run pattern. Passport.authenticate. I think I actually have to return here just to make sure we are catching errors. Okay. So now that the strategy exists we can say passport.authenticate and give it the strategy. And in order to actually call that authenticate function we have to also give it the request and response options. This one's not in the simple documentation here but if you go and read the deeper documentation of the passport that is what we're gonna find. That needs to be if there's no strategy. Let's fix that. Okay. This should kick off the flow now. So if we actually go try to visit this, well it's not gonna work yet because we don't have this function written yet. This will kick off the flow assuming this function exists. Passport.authenticate will send the redirect out to the open ID configuration, open ID server that's configured. They're gonna log in there and then that server will redirect them back to the callback URL. Which means we now have to finish the passport authentication which looks very similar. Passport.authenticate strategy. And then here instead of just that parameter we're gonna also say what happens when it's successful. Because we actually wanna send the user back to the front end after it's successful. So here we can pass in success redirect. And here I'm cheating again by hard coding the front ends URL which is localhost 3000. So again, this would ideally be a configuration item somewhere but it's good enough for now. And then we have to also pass the same request, response and the next handler. Okay, before we can actually test this out we need to write this function. And this is kind of the magic that's gonna make our multi-tenant thing work. So let's go out of spot for it up here. Create strategy and it's going to be past an org. We're giving it the org record from the database. Create strategy. This needs to actually go ahead and create the open ID connect strategy with the items and the configuration from the database rather than hard coded. Here we can say return new open ID connect strategy. And this is a passport thing. I think we need to actually make sure we import that. So let's make sure we have everything we need imported up at the top. We're gonna import passport OIDC. It's better up there, we'll do that. We're going to then make sure we have a variable called open ID connect strategy just like we have local strategy. So open ID connect strategy is that strategy from passport OIDC from up here. We're gonna create the open ID connect strategy. Now this is gonna look similar to what we find in the docs here except instead of hard coding them, we're gonna use the records, the values from the database. So the required items for configuring this are gonna be issuer. We have authorization URL, which is the authorization endpoint with the token URL. We have the user info URL. And then we have client ID and client secret. We also have to define what scopes we wanna ask for, which we're gonna ask for their email and profile info. And we have to pass a callback URL into this, which is again, we're gonna do localhost. This is the backends URL. So it's 3333 slash open ID slash callback slash the org ID. Let's go fill this in. So this is just gonna be org.issuer. These are our column names, right? So we have org.issuer, authorization endpoint, token endpoint, user info endpoint, client ID and client secret. Authorization endpoint, org.token endpoint, org.user, info endpoint, org.client ID and org.client secret. And then for the scope, we're gonna say open ID, or no, that's default, profile and email. What it'll actually do is it'll actually send the scope open ID plus whatever I send here. So we're gonna ask for the user's profile info, which will return their name and possibly other things from depending on the open ID connect server and email address. We definitely do want to know their email address. Okay, so that's the first part of this. Second part of this is that verify function. So here we have to define the verify function. And this will pass, it'll pass these three values in. Now we're using the passport JS library, depending on what other library you're using in whatever framework or language you're writing this in, you'll probably have something similar. It may not work exactly like this, but basically look for how the library wants to hand control back to your code after it deals with the open ID part. And now we're going to, now we're gonna go ahead and fill this in and do our own logic of what happens after a user logs in. We're gonna keep it simple for now just to test it. And then we're gonna go back and fill in a couple of the edge cases we need to handle. So I'll just leave this comment here, which is that passport is going to run this function after it completes the OIDC flow. So for now, at the end of this space, we basically have to return a user record and that's the user that's gonna be logged in by passport and the library all underneath. So we need to create a user record that is going to be the user who's logged in. So similar to how we had the password login, if I go look at the, let's go look for the how passport deals with local users. Here we had the local strategy for username and password login. So what passport is doing is it's giving the username and password from the front end into this library and this is gonna do the query where we're doing our horrible, horrible lookup email, lookup users by their email and plain text password. But this is our own ORM, passport doesn't really know about this, but we're returning this user record. So back down here, what we need to do is basically just create that user object based on who logged in. This time, their profile info is in this profile variable and we don't have a password to check. If we get to this part of the code, it means that OpenID Connect was successful against that issuer. So now we know that this issuer, that OpenID Connect server says that or this user has authenticated. So the sort of simplest cases, we can assume that we don't have any user for this, any local user for this remote user in our database yet, and we can go ahead and create the user. So this is gonna be kind of the simple case, which we're gonna have to fix later cause then we will have users eventually coming back. But the simple case is let's go and create the user. So here's our ORM, we're gonna create the user. We are going to connect them to the organization, which was provided up here, so that's just the org ID. We're gonna set that external ID column, which we added before, right? So now our users have a external ID, which is the identifier of this user at the remote server. We are gonna have their email, which comes back as a list and we just only care about the first for now, which is more edge cases they handle later, but let's just get it working and we should hopefully get their name, their display name. So this, if this is successful, when we do an OpenEconnect login, it will create a new user in the database. Now, again, we're gonna have to fix this up cause if they come back, it's gonna try to create another user with them, with that same info and things are gonna break. So to do, fix this for returning users. Just leave that there for now just to see if this works. So if everything worked, we're going to run that callback URL, our callback method with no error and we're gonna pass it the user that we just created and then passport will deal with setting the local cooking that logs them in. So this is getting a lot closer. Now it's time to actually go and plug in some real OpenID configuration to make this actually work. So for this part of the exercise, you need to pick an OpenID server that you want to use as a, to pretend what customer is, like what server the customer has. Basically it's whoever your customer is, they're gonna say like, here's my OpenID Connect server and you'll need to go register a client at that server, plug in the client ID and client secret and plug in all the configuration for that server. That's gonna be something that you don't necessarily control. So this is again why standards are good because hopefully if they support OpenID Connect, you can just plug it in and it should just work. For this exercise, we're going to use an Okta org. So we're gonna assume that your customer has an Okta organization that they're using for single sign-on into all of their applications. Here we need to now go create a Okta account so that you can actually try this out. What we're gonna do now is go create an Okta developer account. That is a free account you can use for testing all this stuff out. There's no time limit on the length of the account. So this will be how we can actually go and make an OpenID Connect server in the Okta org, which will be simulating what your customers would have already done in their own org. I've already signed up for an account, so I'm not gonna go through the sign-up process, but do go ahead and register if you haven't yet, if you don't already have an Okta account. But I can just go ahead and log in to mine that we're gonna use here. Okay, so I've got myself logged into my Okta admin dashboard, make sure you click on the admin page otherwise you'll see something totally different. And in the admin side, this is where we're gonna go and add the integration for the app. I'm gonna go down to apps, applications, click applications again, and create an app integration. If your app isn't published in the app catalog, then you'll have to go and create the app integration. So we'll click on OpenID Connect and we'll say we're building a web application, which is a server-side application, which is true because our Node.js server is the one doing the OpenID Connect integration. Let's call it to do app and we only need the authorization code flow. Now we need to define the sign-in redirect URL. Now this is a bit of a trick. So in our code, we're using this pattern for redirect URLs, which is OpenID slash callback slash org ID. This means that each customer has their own redirect URL. So in order to actually put this into the OpenID server, we have to know what that org ID is ahead of time. So previously we had added the org record in our database for the white rabbit organization with the ID one, which means the callback URL for that organization will have a one over here. So in the sign-in redirect URL spot in octa, I can go ahead and type in our local host, our backends URL, localhost 3333 slash OpenID slash callback slash one. Now I don't need to use the wildcard feature, which it says it's unsafe anyway, because this customer only needs to redirect to their org's redirect URL on my app. If I go give a redirect URL to a different customer, I'm gonna give them their org's redirect URL. So I might give a different customer the one that says slash two. We're gonna go ahead and scroll down and allow everybody to access the application and click save and we're done. And now we've got a client ID and a client secret. Let's go ahead and copy this back into the application. Oh, right, we're not hard coding it, we're putting in the database. So this is where we're gonna go and manually insert this into the database. In our org table, we had started by creating this organization for the white rabbit company. Now we have to actually provide these real values here. So we're gonna start filling these in. Client ID, we're gonna grab the client secret. We now need to find these values. These are issuer, authorization endpoint token endpoint and user info endpoint. Well, how we can do that is actually in security API. Here's my server and you can see it's telling us the issuer URL here. So I'm gonna grab that, put that in as the issuer. I do need to find the rest of the endpoints though. If you click in, it'll tell you the metadata URL and the metadata URL is where it has a bunch of information about this OAuth server. Let's go grab that, let's grab the authorized endpoint there. Let's grab the token endpoint and let's grab the user info endpoint. Because this is the OAuth server metadata, it doesn't have the user info endpoint because that's actually part of the open ID metadata. But if you replace this OAuth authorization server with open ID configuration, that gives you the open ID metadata, which does have the user info endpoint. So kind of awkward, but we'll just have to move on. We can grab that, plug that in here and let's save this change. So at this point, we have our first customers open ID configuration plugged in. And this should be enough to actually try this out. Let's see what's going on. I have an error in the code. Looks like I forgot to make this org for my D and async function. So let's fix that. This needs to be an async function. Let's see if it likes that. Apparently, apparently it's getting late. Async function, not function async. Let's try this again. Cannot find name user. What did we do wrong with the user there? That was returning. Oh, I think we forgot to actually define it. Do that. Oh, interesting. We're getting an error now. This is relevant. Password is missing in our query here. What is this? It's saying we can't create a user without a password, which is, we do wanna create a user without a password because there is no local password anymore. So I think what we need to do is actually change our schema to allow the password column to be blank. So just like we have external ID as an optional value, like it allows null because some users will have a password but not be external, we also need to make the password column optional because now our users that exist from an OpenID server are not gonna have a password in our own application. Let's save that and we do need to update the schema now. NPX Prisma Generate and let's restart the API. I think we're running. So let's just try the OpenID check again. A username that does not exist correctly returns null. Let's check our trinity at white rabbit should return or gritty one. So let's give it a try on the front end. Let's go ahead and reload this and say a trinity at white rabbit dot fake and cross your fingers. Okay, time to troubleshoot. Let's see what happened on the command line. Need to make this a little bigger so we can read the error messages. Looks like OpenID connect requires an issue or option. So something is not working with grabbing the configuring our strategy. So this is in, let's see if we can figure out what we did wrong. I put that org look up in a function and I forgot to run a wait. Now we need to let that complete. Let's go ahead and actually give us a shot. We're gonna say trinity at white rabbit dot fake and click sign in. With any luck, this should take us to the OpenID connect server and we should be signing in. Okay, something happened. I'm guessing it crashed. I'm guessing that means there's an error in the console here. Oh, one more thing I forgot to do. I don't mind debugging this live. It's useful because errors happen. So I forgot that actually just changing the schema doesn't actually change the database. We need to actually apply the schema changes to the database using the migrate function. So here we're gonna do npx prisma db push which should actually sync the real database with the schema file. So that now the actual password fields should allow null values. So let's try this again. Go back to the front end. Go ahead and type trinity at white rabbit dot fake. Click sign in and hey, look at that. We signed in. It actually happened pretty quick. I got sent out to octa and then back so fast you didn't even see it. Let's try this again and make it a little bit more obvious. We're gonna sign out. What I'm gonna do is I'm actually gonna sign out from my octa org as well. And I did say that we have another to do which is fixing the case when a user comes back. So I'm gonna actually go ahead and go into my user table and delete the user that was created. Let's restart prisma studio. And I'm gonna go ahead and actually delete this user that got added. We'll come back to that in a minute. That's just gonna let me demo this again. So I'm logged out of octa and I'm logged out of the to do app. So now I'm gonna enter trinity at white rabbit dot fake. That's going to ask the back end if I have an org configure for that domain. If so, it'll redirect me to the octa org. And now I get a sign in prompt which is what I expected to see. I'm gonna go ahead and enter my email address for my octa org that I used to sign up, click sign in, got redirected back and oh, what is this? Interesting problem here. There is one more change we have to do which is how cookies are handled. So by default, the browser actually isn't going to send my apps cookies to the app if it was redirected from a different domain. And that is actually an attribute on the cookie that was created up here called same site. You probably do want strict same site cookies for most cases when you're just dealing with your own one website. And this is probably what you have in your current application. Because we need to now let the browser send the cookie to the application when it came back from a redirect from open eConnect, we now need to actually change this to lax and that will allow it to be sent. If we look at the request console for this, it would actually show that the browser didn't send a cookie to this callback URL and that's that cookie policy, the same site policy. So this change should allow this to work. In this case, the open eConnect flow did not complete so we don't have a user record in the database to delete so let's just start over. I'm still going to log out of my OctoOrg because I want to show you what happens when we actually go through from scratch. So logged out of both. We're gonna say trinity at white rabbit dot fake. We'll click sign in. Now we have to log into the OctoOrg. We'll come back to what's happening with my different email address here in a little bit. And this time it worked with the new cookie policy in place. My browser was able to send the cookie along from that previous session, which then let us actually finish the flow. So what just happened? I got logged in, we created a new user record. If we reload this table, we'll see a new user record created which was in that verify function. That user has an email address which interestingly is different than the one that I typed in but I'll explain that. No password. It did find my name and it did associate me with this organization. But most importantly, it found my external ID which is the octa user identifier for this user. And you'll notice that it's not like a human significantly readable string. It's just like a random set of letters and numbers which makes sense because this is a stable identifier for the user as opposed to my email address which as a user, I can go change my email in my octa account. So back to this whole thing. Like if I entered this email, why did I get logged in as a different email? Well, that's because the OpenID Connect server ultimately said this is the user that logged in with this external ID. So I don't have a user with that external ID so it had to go make one. And it made one with this email address which was the email on my octa account. So now we need to actually go and fix up our application to handle the case of users logging back in. And this is where there's a couple of interesting edge cases to handle. This is also why we had to make the email column not unique anymore because it's very possible that a user exists in multiple identity providers with the same email. And we wanna actually treat them as different users because otherwise there's a chance that one identity provider can just start claiming to be people from a different company and then they would be able to see each other's data. So that's not good. And that's why we're scoping our logins to the combination of org ID and external ID, external user ID, so that we always know that a user at this customer's IDP can't claim to be users out of different customers identity provider. Okay, back to our verify code. Right now, all we're doing is creating a new user every time an opening login is successful which is obviously not correct. So how are we gonna fix this? Well, let's fix the simple case first. The simple case is that we've already created a user this way which means we know their external ID in our database. So that one we can solve. We're gonna go ahead and look for a user that matches that. We're gonna say user is await and then we're gonna do a query. So prisma.user.find first and the query is where we can't only match our external ID because again, the external ID is scoped to that identity provider. We do want to include it but we also have to include the org ID in the query. So we're saying find users from the org that is our callback URL is running from that match this external ID. If they don't exist, then we can create them. So this will handle things a little bit better. This means we're first gonna look for a returning user. If they don't exist, we will create a user in that org. This should be enough now to let me log back into this application multiple times. Let's just double check. So here I've got a user with an email address interestingly at a different domain which is again, okay, because we wanna handle that in case for whatever reason we're in this situation. So I'm gonna go ahead and actually now use this when I log in. I'm gonna use that email address. I've got an external ID. So let's log out, type that in. That's gonna ask the backend, do you know about a user with this email? And if you do, what org do they belong to? Which we do have this user now in the database and they belong to org number one. So that let's actually sign out of this again just to make sure that we're really, really logged out. I'm gonna sign out of my octet account. This is kind of, it's important to do this when you're testing otherwise you may not even realize a redirect is happening because it's actually really fast. Click sign in, oh shoot. Oh, I need to not define that as a constant. That is now just a regular variable because we're gonna be reassigning it throughout the rest of this function. Let's try this again. Restart the app. Let's restart the front end and try this one more time. Enter my email address and click go. We get redirected to the identity provider. I'm gonna log in and we get redirected back. And sure enough, I am there and I have the same ideas before. So there is not another copy of me. It's found the same one. One other edge case here, similar to how we were typing in the Trinity address before and then I got this user created. I'm not gonna log out of octet this time. If I enter anything at whiterabbit.fake, obviously this email doesn't exist anywhere but the domain still matches. So we do still wanna redirect them to the identity provider and now I'm logged in as my normal account with the same external ID. Really what this means is that that first email prompt is really more of just a suggestion when it matches a customer's domain. We don't really care what they entered here as we're gonna use whatever comes back from the identity provider in the redirect. The other totally different way to handle a login screen like this, by the way, is instead of asking for an email address, you could just ask for their domain, like their company domain. You could just say what is your company domain that's registered here and it would be whiterabbit.fake. That's not gonna work because we're looking for an email, but all they really have to do is enter the domain and then we redirected their identity provider. So you can have it work either way, it's up to you. It's just that it looks, I would say a lot of people end up doing it this way where it's just an email address and then you either ask for a password or you match it with a identity provider. So we can sign in successfully now with our actual email that doesn't match a company domain because it's already manually associated with an org or any email address at that org and it'll just be logged in as whatever user is actually logged in as at that IDP. But there's one more edge case. So one more edge case. In order to actually demonstrate this, I need to set up a situation where, which actually is what Trinity's account is in right now. So the domain of the company is white rabbit dot thing, but Trinity has been using this app already for quite a long time, which means this record does not have an org ID in it because it has been an individual account. However, now there is a company account for this domain. If we leave our code as is right now, what's gonna happen is Trinity's gonna log in. The identity provider will say this is the email address, but before we do anything, we're gonna check, do we have a user that matches Trinity's external ID? We don't know Trinity's external ID because she's never logged in through OpenEconnect before. So it'll say no, there is no user that matches that and it'll go and create one. That will create a new record for Trinity here, which is a bit of a problem because then it's a totally new user as far as the app is concerned and all of the data will be empty. So she's just lost all of her work. So what we need to do is be able to link up existing users that now belong to a company domain. So that's this last edge case to handle. So I've just gone into my Okta.org and I've created a new person with the email address, Trinity at white rabbit dot fake, which is what we've been using in our demo here. This means I have an actual user record here that I can use and log in at Okta into our application. So I'm gonna log out of my admin account and now let's go ahead and actually try this to demonstrate the problem that our app has to demonstrate the edge case. Let's sign out of the application, log in as Trinity at white rabbit dot fake. That's going to do the same thing as before, which is redirect over to Okta. This time I'm gonna log in as Trinity and now we're logged in. But here we have user six and that's because of this problem where our code was not looking for the case of an existing user that doesn't have the org set yet. So let's fix this up. Let me log out. Let's delete this extra record and now let's go fix this in our code. So we're gonna first look for, do we have a user at this organization with this external ID, which is the canonical case? And if that's true, we wanna just use that user and skip everything else. But if that doesn't exist, we don't wanna necessarily create them first. So first we're gonna say, if there is no user, we're gonna do something else instead. So first, what we're gonna do is we're gonna check, is there a user on the organization with a matching email rather than a matching external ID? So again, we're gonna do a query. We are gonna query on the org ID, but we're gonna say, is there a user that matches the email instead of matching the external ID? If that does exist, we want to update their record with the external ID. So here we can do user.update and we're gonna say where the ID is the user ID. We're gonna update the data external ID, which will be profile.id. So this is gonna say, update that user, set their external ID that way when they come back, we don't have to do this check again and that'll let them change their email address if they ever do and we won't be doubling up their accounts again. The one thing that I will note here is that this actually won't currently work because again, the whole thing that led to this problem was the fact that Trinity's record user ID two doesn't have an org ID set. We're doing this the safe way here, which is that we're gonna have to write a migration script which will actually go find any user at this company domain and update it with their org ID. And that's gonna be this thing, like basically part of the onboarding of this customer. So new customer signs up, you go and plug in their opening configuration into your organization table, grab the org ID and now we say, okay, let's go look for matching users who might have already been using the app and update them to make sure that they're now associated with that org record. So we're gonna provide that script. Let's put that off for later and instead test this part of the code by just setting the org ID manually here, assuming the migration script has already run. So migration script is run, it's updated Trinity's record with the org ID, which we know because it's a matching email domain. However, we still don't know the external ID for that user, which we can't know because we're not actually querying that authorization server. So now our code is going to say, is there a user at this organization one with the matching external ID? Well, we don't have this external ID yet. So no, there is no user. We're gonna find a user at this org that matches this email. And if that does exist, we'll update the external ID column so that we can hit this first case later. And then there is a user so we're not querying user and we're good. So let's double check this all works and then keep going. I'm gonna log out. We're gonna say, Trinity at white rabbit. I'm already logged into Octa so it should just come right back. Great, and look at that. This, I'm now logged in as user two. So I've kept all of my to do items and now there is an external ID column or a value set on that column. And we can see the next time I log in as Trinity, I will go through the simpler case of just matching on the external ID. Okay, this is better. This is good. We're very close. So at this point, you now have things set up where you can create multiple OpenID configurations for multiple different companies at different domains. Any user record now can be associated with that organization and it'll send them out through their own OpenID server. You'll match users on external IDs in case they, that'll let them change their email address at the identity provider without messing up your version of them. And we're not doubling up users. We're letting people log back in and we're gonna have to now create that migration script which will be the last step. So how do we make a migration script? Well, let's go back to our code here. And we kind of actually have one already that we used to seed the database with our fake data in the first place. So we're gonna kind of like mirror this and just make a new version of it which will be the maintenance task of onboarding a customer. And this is something that you will have to do every time a new customer signs up because the reality is that you do have existing users in your database that may now be associated with an identity provider. So let's just make a new script in this folder. Let's call it oidcscript.ts and we need to go into our package, Jason, and actually make a little command for it. So just like we had the initdb which runs that seed script, we're gonna make a new one here called oidcmigrate. And here we're gonna do npxtsnode and then we'll give it the file. So prisma oidcscript.ts. So this will give us a npm command oidcmigrate which will run that file. What are we gonna do in that file? Let's set up some boilerplate stuff which is importing the prisma client and connect to the database. And we're gonna have our main function and now we're gonna provide a command line argument. And what we're gonna do is we're gonna say run this like this, we're gonna say npm run oidcmigrate and then example.com. That'll be the customer domain. So here we need to now check, do we have an org with this domain? So we'll just set this as, so we'll just grab the domain from the list of arguments and then we'll say org. And now we're gonna do the org parameter is either prisma.org.find first with a matching domain. That looks better like that. Oh my gosh, typing, typing is hard. And if there is no org found, then we're gonna throw an error. We need to add a little bit more boilerplate stuff down here to actually run that function and deal with the prisma connections and catching the errors. Okay, let's give this a shot. Let's run this with npm run oidcmigrate with no arguments. Good, we got an error. So let's run this with a domain that does not exist or not found. And if we run it with a domain that does exist, we should get the org info spit out here. Great. So now we can actually make this work. We're gonna do a couple of things here. We're going to find all of the users that match the email domain of this org. So this is where we're gonna update them to include, update the org.icom on that record. So here we're gonna say user, users are prisma.user.findmini. And we're gonna say where email ends with at domain. And now we can loop through them all. Each, and now we'll update them. So wait prisma.user.update, the is the user ID, and we'll update the org ID to be the organization's ID. And we actually wanna get rid of their password because we don't want any password for them anymore because they can no longer log in with the password. So we'll set that to null. So now we're done. And one of the other things we should do because again, if we go back to our schema, we added the org to the to-do list items as well. Let's go ahead and just add a couple of to-do items in here so that we can double check the migration is working. So I'm logged in as Trinity, so I can say one, two, three. And if we look at our database, we have to-dos that belong to the user but are not associated with an org yet. This is what we again might have as a realistic scenario of you have a bunch of users at the company but they're individual accounts and now you wanna migrate those users and their data to belong to that company. So we're gonna do the same kind of migration with the to-do table. We're gonna find all of the to-dos that belong to all those users and then we're gonna update the org ID column. So now that we've migrated our users to belong to the org, we can do a nested query. So we're gonna say grab all the to-dos from all of the users that match this org and update the org ID. This one's a bit of a complicated Prisma query so I'm just gonna copy it from my notes here. If you're curious about the Prisma syntax for this, you can go look it up on their docs. What this is doing is saying find all the to-dos where the users match an org and then update the org ID of the to-do column. So we're gonna run the migration script with the domain white rabbit dot fake. And what happened? I did have a typo. Cannot find user, did you mean users? Yes, I did. I did in fact mean users here because that was what I said up here. Try this again. Okay, no errors. Let's double check our database. We go look at our users. We have, I guess I should probably reset this. Let's reset that and reset that. Let's just make sure the migration script catches that user. Great. Okay, good. So it did set the org ID. We don't know the external ID. That'll only get fixed when we log in next. But now all that's to do is we just had, which we're associated with the user too, should now be associated with an org and sure enough, they are. So if I go log out and log back in, we should update our record with the external ID. So we see external ID here and we see the external ID there. Okay. I think we're just about done. Let's go ahead and recap all the changes we did. And let's just look through our diff of our get history here. So first of all, schema changes. Here we've added the org ID to the to do items themselves. We've also added an org column to the user database and made passwords optional and the drop the unique constraint on emails since that's no longer our unique identifier. Now the unique identifier for users is the combination of org and external ID. The external ID is a string, which is optional, which will be the external identifier for that user. We've also, of course, added the entire org table with domain, which is the most important thing for the customer. And then their open ID configuration and API key in case we want to be able to do things at the org level with an API and the org has to do is end users. As far as our package, Jason, we've got this new script for the migration and we've also added passport open ID connect. Back to the front end changes. We've added a section here to what happens when they click login, which is if there's no password entered. So when they just enter their email, which is first, we're gonna ask the backend if we know about that domain name. And if so redirect to the back ends to start the OIC flow. Otherwise show the password field here. Of we've hidden the password field by default. And then of course, that's the other part of that, hiding the password by default. And that's really like the only front end changes. The guts of the front end change are here, which is this new function that's going to go and ask the backend. Do we know about the domain name for this email? Most of the work is done in the backend. So we had to bring in the OIC library. We had to change the way our cookies are stored. The same site is now lax, which lets the cookies get sent even if redirected to from a different domain. And most of the code is here. So this is just a simple function to grab the second part of an email address after the at sign. You might have a better way to do that with a built-in function in your language. That's fine. Here's our open ID check, which is going to say given a email address, do we know about a org in our database that matches that email? This is the part that makes the actual open ID flow work where we're going to create that open ID connect strategy on the fly from values in the database, which is the kind of unusual thing here compared to hard coding this like you might normally do. The verify function is part of passport JS. This is the function that passport calls after a user logs in. And this is where we're handling a couple of interesting edge cases now, looking for a matching user that we already know about, the matches that org and an external ID. If they're not found, we're going to look for a user on that org that has a matching email, which is going to handle our migration case of people who already had an account at our app who are now logging in with a corporate account. We'll update their external ID so we know about them in the future. If those still don't exist, it means it's a new user trust we haven't seen before. So we're going to create a record in our database for that user. This is also where you might do any other things like onboarding new users. So if you do anything where you're onboarding users, you would hook into this spot and I'll do those other things too. Like maybe you send them a welcome email or send them some tips the next day or whatever it is. This is the actual guts of the open ID flow, which is really just telling passport to start and finish the flow. So here we're doing the open ID start, creating that strategy given the org that was returned from that ID. And then this is the callback URL that the user gets sent back to from the open ID server where we're going to say, what happens after this is done? We're going to redirect them back to the front end. Lastly, we also have the migration script, which is going to find every user that matches the company domain that we just enrolled, update those users, find any to-dos that belong to those users and also associate them with that org. And that's it. Now there are a few more edge cases that you probably want to handle if you were doing this in production. Let's talk about a couple of the extra edge cases that you might care about. One thing that we don't have support for in this model of our app at all is a user belonging to multiple orgs. The way we've got the database set up, a user can belong to a single org. That's kind of the simple case. Maybe that's what you want, but it's also possible. You may want a user to be able to belong to multiple orgs. That will require more changes on your end, really. It's more just internal schema changes and things. So it's up to you as to whether that's important. But that can add some complexity. One other possible way this gets a little bit more complex is what happens if the customer's OpenID Connect server behaves slightly differently than how you expect it to from the library you're using. OpenID Connect is a standard. Everyone's supposed to follow the standard, but in practice there are sometimes slight differences with things. So PassportJS supports standards-based OpenID Connect. So any standards-based OpenID Connect server should hopefully work with it just fine. But you may end up with customers with slight variations depending on what product they're actually using. Another slight complication and a slight edge case around OpenID Connect is some OpenID Connect servers will return user profile info in the ID token itself. Others will force you to make a call to the user info endpoint. And that abstraction, thankfully, is already handled by the Passport library. But if you're not using Passport itself, that's another difference you'll have to be aware of. And then there's the whole SAML thing. SAML is an alternative to OpenID Connect. It's a lot older. It's all based in XML instead of JSON. And in spirit, it's similar. You're still gonna send the user over to the server and then get redirected back. And ideally, a library is doing this so in practice it may actually look similar to just using Passport.js. But now you also need to handle the case of a SAML configuration set up for that org as well. So in your org database, you would have to be able to say, this org is an OIDC based org. This one is a SAML org. Here's all the SAML config columns, things like that. So these are just a few of the things to keep in mind as you're adding support for multi-tenancy and letting your users log in through their own company's identity providers. I hope this has been helpful. The complete source code that we just walked through is available on GitHub. So feel free to check that out and run the finished product. Try to follow along. If you need to steal things from the finished product to follow along, that's totally fine. I hope you can get it up and running with an Okta account for extra credit. Try it with a totally different identity provider and see if you can also make it work or try it with a different Okta org. All these other things that you might encounter in the real life once you actually start deploying your application. All right, that's it. I hope you had a good time in this workshop and I look forward to seeing you in the next one.