Skip to content

Commit e2eccb2

Browse files
authored
Merge pull request #40 from Eiromplays/feature/add-microsoft-entra-id-example
Microsoft Entra ID OAuth Example
2 parents 7883e7d + bf8445e commit e2eccb2

File tree

4 files changed

+139
-1
lines changed

4 files changed

+139
-1
lines changed

dev/.env.example

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,32 @@ NEXT_PUBLIC_URL=http://localhost:3000
2828
# optional: google oauth2 client secret, not activated if not set
2929
# ZITADEL_CLIENT_SECRET=
3030

31+
################################################################################
32+
# Microsoft Entra ID OAuth 2.0 config
33+
################################################################################
34+
# optional: Microsoft Entra ID client id, not activated if not set
35+
# MICROSOFT_ENTRA_ID_CLIENT_ID=
36+
37+
# optional: Microsoft Entra ID client secret, not activated if not set
38+
# MICROSOFT_ENTRA_ID_CLIENT_SECRET=
39+
40+
# optional: Microsoft Entra ID client secret, not activated if not set
41+
# MICROSOFT_ENTRA_ID_TENANT_ID=
42+
43+
# optional: Microsoft Entra ID administrator group id, activated if not set
44+
# MICROSOFT_ENTRA_ID_ADMINISTRATOR_GROUP_ID=
45+
46+
# Note: For Microsoft Entra ID, you need to:
47+
# 1. Create an app registration
48+
# - Go to Azure Portal -> Microsoft Entra ID -> App Registrations -> New Registration
49+
# - Fill in the name and select the supported account types
50+
# - Add a "Web" redirect URI: http://localhost:3000/api/users/oauth/microsoft-entra-id/callback
51+
# - When created, go to API Permissions -> Add a permission -> Microsoft Graph -> Delegated permissions -> Select the ones you need, e.g. email, openid, profile and offline_access -> Add permissions
52+
# - Optional: If you do not want users to have to give consent to your app everytime they login: Click on Grant admin consent for {tenant} -> Yes
53+
# - Optional: If you want groups to be part of your token(s), you can go to Token configuration -> Add groups claim -> Select the groups you want to add -> Save
54+
# - Go to Certificates & secrets -> Client secrets -> New client secret -> Add a description -> Expires -> Add -> Copy the secret (it will only be shown once) -> And save the secret somewhere safe or add it to your .env file
55+
# You can read a little about registering apps here as well: https://learn.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app
56+
3157
################################################################################
3258
# Apple OAuth Config
3359
################################################################################
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
"use client";
2+
3+
export const MicrosoftEntraIdOAuthLoginButton: React.FC = () => (
4+
<a href="/api/users/oauth/microsoft-entra-id">
5+
<button
6+
className="btn btn--icon-style-without-border btn--size-large btn--withoutPopup btn--style-primary btn--withoutPopup"
7+
style={{ width: "100%" }}
8+
>
9+
Continue With Microsoft Entra Id
10+
</button>
11+
</a>
12+
);

dev/src/payload.config.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { buildConfig } from "payload";
55
import sharp from "sharp";
66
import { fileURLToPath } from "url";
77
import { googleOAuth } from "../../examples/google";
8+
import { microsoftEntraIdOAuth } from "../../examples/microsoft-entra-id";
89
import { zitadelOAuth } from "../../examples/zitadel";
910
import LocalUsers from "./collections/LocalUsers";
1011
import Users from "./collections/Users";
@@ -23,6 +24,7 @@ export default buildConfig({
2324
afterLogin: [
2425
"src/components/GoogleOAuthLoginButton#GoogleOAuthLoginButton",
2526
"src/components/ZitadelOAuthLoginButton#ZitadelOAuthLoginButton",
27+
"src/components/MicrosoftEntraIdOAuthLoginButton#MicrosoftEntraIdOAuthLoginButton",
2628
],
2729
},
2830
user: Users.slug,
@@ -35,6 +37,6 @@ export default buildConfig({
3537
editor: lexicalEditor({}),
3638
collections: [Users, LocalUsers],
3739
typescript: { outputFile: path.resolve(dirname, "payload-types.ts") },
38-
plugins: [googleOAuth, zitadelOAuth],
40+
plugins: [googleOAuth, zitadelOAuth, microsoftEntraIdOAuth],
3941
sharp,
4042
});

examples/microsoft-entra-id.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { PayloadRequest } from "payload";
2+
import { OAuth2Plugin, defaultGetToken } from "../src";
3+
4+
////////////////////////////////////////////////////////////////////////////////
5+
// Microsoft Entra Id OAuth
6+
////////////////////////////////////////////////////////////////////////////////
7+
const clientId = process.env.MICROSOFT_ENTRA_ID_CLIENT_ID;
8+
const clientSecret = process.env.MICROSOFT_ENTRA_ID_CLIENT_SECRET;
9+
const tenantId = process.env.MICROSOFT_ENTRA_ID_TENANT_ID;
10+
const administratorGroupId =
11+
process.env.MICROSOFT_ENTRA_ID_ADMINISTRATOR_GROUP_ID;
12+
const serverUrl = process.env.NEXT_PUBLIC_URL || "http://localhost:3000";
13+
14+
const strategyName = "microsoft-entra-id";
15+
const tokenEndpoint = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`;
16+
const authorizationUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize`;
17+
const microsoftGraphBaseUrl = "https://graph.microsoft.com/v1.0";
18+
19+
export const microsoftEntraIdOAuth = OAuth2Plugin({
20+
enabled:
21+
typeof clientId === "string" &&
22+
typeof clientSecret === "string" &&
23+
typeof tenantId === "string",
24+
strategyName,
25+
useEmailAsIdentity: true,
26+
serverURL: serverUrl,
27+
clientId: clientId || "",
28+
clientSecret: clientSecret || "",
29+
authorizePath: `/oauth/${strategyName}`,
30+
callbackPath: `/oauth/${strategyName}/callback`,
31+
authCollection: "users",
32+
tokenEndpoint: tokenEndpoint,
33+
scopes: ["openid", "email", "profile", "offline_access", "User.Read"],
34+
providerAuthorizationUrl: authorizationUrl,
35+
getUserInfo: async (accessToken: string, req: PayloadRequest) => {
36+
const userInfoResponse = await fetch(`${microsoftGraphBaseUrl}/me`, {
37+
headers: { Authorization: `Bearer ${accessToken}` },
38+
});
39+
40+
/*
41+
* The following is an example of how to check if the user is a member of a specific group.
42+
* You can use this to restrict login when not part of the group, or to restrict access in payload.
43+
*/
44+
const groupsResponse = await fetch(`${microsoftGraphBaseUrl}/me/memberOf`, {
45+
headers: { Authorization: `Bearer ${accessToken}` },
46+
});
47+
const groups =
48+
(await groupsResponse.json()) as MicrosoftGraphGroupsResponse;
49+
50+
const groupIds = groups.value.map((group) => group.id);
51+
52+
if (administratorGroupId && !groupIds.includes(administratorGroupId))
53+
throw new Error("You are not authorized to access this application.");
54+
55+
const user = await userInfoResponse.json();
56+
return { email: user.mail, sub: user.id, name: user.displayName };
57+
},
58+
/**
59+
* This param is optional to demonstrate how to customize your own
60+
* `getToken` function (i.e. add hooks to run after getting the token)
61+
* Leave this blank should you wish to use the default getToken function
62+
*/
63+
getToken: async (code: string, req: PayloadRequest) => {
64+
const redirectUri = `${serverUrl}/api/users/oauth/${strategyName}/callback`;
65+
const token = await defaultGetToken(
66+
tokenEndpoint,
67+
clientId || "",
68+
clientSecret || "",
69+
redirectUri,
70+
code,
71+
);
72+
73+
if (req.user) {
74+
await req.payload.update({
75+
collection: "users",
76+
id: req.user.id,
77+
data: {},
78+
});
79+
}
80+
81+
return token;
82+
},
83+
successRedirect: (req: PayloadRequest, accessToken?: string) => {
84+
return "/admin";
85+
},
86+
failureRedirect: (req, err) => {
87+
req.payload.logger.error(err);
88+
return "/admin/login";
89+
},
90+
});
91+
92+
type MicrosoftGraphGroupsResponse = {
93+
value: MicrosoftGraphGroup[];
94+
};
95+
96+
type MicrosoftGraphGroup = {
97+
id: string;
98+
};

0 commit comments

Comments
 (0)