├── src
├── middleware.ts
├── app
│ ├── favicon.ico
│ ├── api
│ │ ├── auth
│ │ │ └── [...nextauth]
│ │ │ │ └── route.ts
│ │ └── app
│ │ │ └── route.ts
│ ├── globals.css
│ ├── layout.tsx
│ └── page.tsx
└── auth.ts
├── public
├── vercel.svg
├── window.svg
├── file.svg
├── globe.svg
└── next.svg
├── next.config.ts
├── renovate.json
├── postcss.config.mjs
├── tailwind.config.ts
├── .env.local.example
├── .gitignore
├── tsconfig.json
├── package.json
└── README.md
/src/middleware.ts:
--------------------------------------------------------------------------------
1 | export { auth as middleware } from "@/auth";
2 |
--------------------------------------------------------------------------------
/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/descope-sample-apps/3rd-party-sample-app/HEAD/src/app/favicon.ico
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/api/auth/[...nextauth]/route.ts:
--------------------------------------------------------------------------------
1 | import { handlers } from "@/auth"; // Referring to the auth.ts we just created
2 | export const { GET, POST } = handlers
--------------------------------------------------------------------------------
/next.config.ts:
--------------------------------------------------------------------------------
1 | import type { NextConfig } from "next";
2 |
3 | const nextConfig: NextConfig = {
4 | /* config options here */
5 | };
6 |
7 | export default nextConfig;
8 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": [
4 | "local>descope-sample-apps/renovate-config"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/public/window.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/file.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | :root {
6 | --background: #ffffff;
7 | --foreground: #171717;
8 | }
9 |
10 | @media (prefers-color-scheme: dark) {
11 | :root {
12 | --background: #0a0a0a;
13 | --foreground: #ededed;
14 | }
15 | }
16 |
17 | body {
18 | color: var(--foreground);
19 | background: var(--background);
20 | font-family: Arial, Helvetica, sans-serif;
21 | }
22 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | export default {
4 | content: [
5 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
6 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
7 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
8 | ],
9 | theme: {
10 | extend: {
11 | colors: {
12 | background: "var(--background)",
13 | foreground: "var(--foreground)",
14 | },
15 | },
16 | },
17 | plugins: [],
18 | } satisfies Config;
19 |
--------------------------------------------------------------------------------
/.env.local.example:
--------------------------------------------------------------------------------
1 | AUTH_SECRET="" # Added by `npx auth`. Read more: https://cli.authjs.dev
2 | CLIENT_SECRET="" # The client Secret of the configured 3rd party application within Descope.
3 | CLIENT_ID="" # The client ID of the configured 3rd party application within Descope.
4 | BASE_URL="https://api.descope.com" # The custom CNAME URL of your Descope project. If not configured, leave as is.
5 | PROJECT_ID="" # The project ID of your Descope project.
6 | SCOPES="openid" # The scope of the user. If not configured, leave as is.
7 | CONSENT_SCOPES="" # The consent scope to validate for the user, these would be configured under permission scopes within Descope. If not configured, leave as is.
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.*
7 | .yarn/*
8 | !.yarn/patches
9 | !.yarn/plugins
10 | !.yarn/releases
11 | !.yarn/versions
12 |
13 | # testing
14 | /coverage
15 |
16 | # next.js
17 | /.next/
18 | /out/
19 |
20 | # production
21 | /build
22 |
23 | # misc
24 | .DS_Store
25 | *.pem
26 |
27 | # debug
28 | npm-debug.log*
29 | yarn-debug.log*
30 | yarn-error.log*
31 |
32 | # env files (can opt-in for committing if needed)
33 | .env.local
34 |
35 | # vercel
36 | .vercel
37 |
38 | # typescript
39 | *.tsbuildinfo
40 | next-env.d.ts
41 |
42 | yarn.lock
43 | package-lock.json
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2017",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./src/*"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "oauth-apps",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev --turbopack",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "next": "15.4.10",
13 | "next-auth": "^5.0.0-beta.25",
14 | "react": "^18",
15 | "react-dom": "^18"
16 | },
17 | "devDependencies": {
18 | "@stylistic/eslint-plugin-ts": "^2.11.0",
19 | "@types/node": "^20",
20 | "@types/react": "^18",
21 | "@types/react-dom": "^18",
22 | "eslint": "^8",
23 | "eslint-config-next": "15.0.3",
24 | "postcss": "^8",
25 | "tailwindcss": "^3.4.1",
26 | "typescript": "^5"
27 | },
28 | "packageManager": "yarn@4.5.3"
29 | }
30 |
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import { SessionProvider } from "next-auth/react";
3 | import { Geist, Geist_Mono } from "next/font/google";
4 | import "./globals.css";
5 |
6 | export const metadata: Metadata = {
7 | title: "Create Next App",
8 | description: "Generated by create next app",
9 | };
10 |
11 | const geist = Geist({ subsets: ["latin"] });
12 | const geistMono = Geist_Mono({ subsets: ["latin"] });
13 |
14 | export default function RootLayout({
15 | children,
16 | }: Readonly<{
17 | children: React.ReactNode;
18 | }>) {
19 | return (
20 |
21 |
22 | {children}
23 |
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/src/app/api/app/route.ts:
--------------------------------------------------------------------------------
1 | import { auth, baseUrl, clientId, clientSecret, consentScopes } from "@/auth";
2 | import { NextResponse } from "next/server";
3 |
4 | export const GET = auth(async function GET(req) {
5 | if (req.auth) {
6 | const body = "grant_type=client_credentials" +
7 | "&client_id=" + clientId +
8 | "&client_secret=" + clientSecret +
9 | "&scope=" + consentScopes;
10 | const res = await fetch(`${baseUrl}/oauth2/v1/apps/token`, {
11 | method: "POST",
12 | headers: {
13 | "content-type": "application/x-www-form-urlencoded",
14 | },
15 | body,
16 | });
17 | return NextResponse.json(await res.json(), { status: res.status });
18 | }
19 | return NextResponse.json({ message: "Not authenticated" }, { status: 401 })
20 | }) as any;
--------------------------------------------------------------------------------
/public/globe.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 3rd-party-sample-app
2 |
3 | This is a sample application to get you familiarized with working with Descope's 3rd party sample applications
4 |
5 | ## Getting Started
6 |
7 | In order to launch this app:
8 |
9 | 1. Clone the repo
10 |
11 | ```
12 | git clone git@github.com:descope-sample-apps/3rd-party-sample-app.git
13 | ```
14 |
15 | 2. Set up Descope environment variables in .env.local file
16 |
17 | Either copy the below, or copy the included `.env.local.example` to `.env.example`. Provide the applicable configurations for your project.
18 |
19 | ```
20 | AUTH_SECRET="" # Added by `npx auth`. Read more: https://cli.authjs.dev
21 | CLIENT_SECRET="" # The client Secret of the configured 3rd party application within Descope.
22 | CLIENT_ID="" # The client ID of the configured 3rd party application within Descope.
23 | BASE_URL="https://api.descope.com" # The custom CNAME URL of your Descope project. If not configured, leave as is.
24 | PROJECT_ID="" # The project ID of your Descope project.
25 | SCOPES="openid" # The scope of the user. If not configured, leave as is.
26 | CONSENT_SCOPES="" # The consent scope to validate for the user, these would be configured under permission scopes within Descope. If not configured, leave as is.
27 | ```
28 |
29 | 3. Install dependencies
30 |
31 | ``` bash
32 | npm i
33 | # or
34 | yarn install
35 | ```
36 |
37 | 4. Start the app
38 |
39 | ```bash
40 | npm run dev
41 | # or
42 | yarn dev
43 | ```
44 |
45 | ## Utilizing the Application
46 |
47 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the application.
48 |
49 | ### Initiate a 3rd Party Application Consent
50 |
51 | You can initiate a 3rd Party Consent by clicking on the `Connect Descoper Site`. This will navigate to the `Flow Hosting URL` configured within the 3rd Party Application within Descope.
52 |
53 | #### Partner Application Side
54 |
55 | Once you have authenticated, within the Partner Application Pane of the application, you can then refresh the user's tokens, or sign out of the application. The user's `id_token` is parsed to be human readable, and the access_token and refresh_token are displayed
56 |
57 | #### Partner Backend Side
58 |
59 | Within the right-hand side of the application, you can click the `Get App Token` button to fetch the application's `access_token` which can then be used to query for a user's `access_token` to do stuff on their behalf
60 |
61 | ## Learn More
62 |
63 | To learn more about Next.js, take a look at the following resources:
64 |
65 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
66 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
67 |
68 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
69 |
--------------------------------------------------------------------------------
/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { signIn, signOut, useSession } from "next-auth/react";
3 | import { useState } from "react";
4 |
5 | export default function Home() {
6 | const [appToken, setAppToken] = useState(null);
7 | const { data: session, status, update } = useSession();
8 |
9 | const getAppToken = async () =>
10 | await fetch("/api/app")
11 | .then((res) => (res.ok && res) || Promise.reject(res))
12 | .then((res) => res.json())
13 | .then((res) =>
14 | JSON.parse(
15 | Buffer.from(res.access_token.split(".")[1], "base64").toString(
16 | "ascii"
17 | )
18 | )
19 | )
20 | .catch((res) => res.json());
21 | return (
22 |
23 |
24 |
Partner App
25 |
signIn("customapp")}
28 | >
29 | Connect DescoperSite
30 |
31 | {session && session?.user && (
32 |
33 | Session:{" "}
34 | {JSON.stringify(
35 | {
36 | ...session,
37 | user: {
38 | ...session.user,
39 | id_token: session.user.id_token
40 | ? JSON.parse(session.user.id_token)
41 | : "",
42 | },
43 | },
44 | null,
45 | 2
46 | )}
47 |
48 | )}
49 | {session && (
50 | <>
51 |
update({ refresh: true })}
54 | >
55 | Refresh
56 |
57 |
signOut()}
60 | >
61 | Sign Out
62 |
63 | >
64 | )}
65 |
66 |
67 |
Partner Backend
68 |
setAppToken(await getAppToken())}
71 | >
72 | Get App Token
73 |
74 |
75 | {appToken && (
76 |
77 | App Token: {JSON.stringify(appToken, null, 2)}
78 |
79 | )}
80 |
81 |
82 | );
83 | }
84 |
--------------------------------------------------------------------------------
/src/auth.ts:
--------------------------------------------------------------------------------
1 | import NextAuth, { DefaultSession } from "next-auth";
2 | import { OIDCConfig, OIDCUserConfig } from "next-auth/providers";
3 | import { DescopeProfile } from "next-auth/providers/descope";
4 |
5 | export const baseUrl = process.env.BASE_URL ?? "https://api.descope.com";
6 | export const projectId = process.env.PROJECT_ID ?? "";
7 | export const issuer = baseUrl + "/v1/apps/" + projectId;
8 | export const clientId = process.env.CLIENT_ID ?? "";
9 | export const clientSecret = process.env.CLIENT_SECRET ?? "";
10 | export const scope = process.env.SCOPES ?? "openid";
11 | export const consentScopes = process.env.CONSENT_SCOPES ?? "";
12 |
13 | const DescopeOAuthApps = ( // based on Descope provider
14 | config: OIDCUserConfig,
15 | ): OIDCConfig => {
16 | return {
17 | id: "customapp",
18 | name: "Custom App",
19 | type: "oidc",
20 | style: { bg: "#1C1C23", text: "#ffffff" },
21 | checks: ["pkce", "state"],
22 | options: config,
23 | authorization: {
24 | params: { scope, prompt: "consent", access_type: "offline" },
25 | },
26 | client: { token_endpoint_auth_method: "client_secret_post" }, // required for backend exchange of app token
27 | profile(profile) {
28 | return {
29 | id: profile.sub,
30 | }
31 | },
32 |
33 |
34 | }
35 | }
36 |
37 | export const { handlers, signIn, signOut, auth } = NextAuth({
38 | debug: true,
39 |
40 | providers: [{
41 | ...DescopeOAuthApps({ issuer }),
42 | clientId,
43 | clientSecret,
44 | }],
45 | callbacks: {
46 | // we use the `jwt` callback to control what goes into the JWT
47 | // https://authjs.dev/guides/refresh-token-rotation#jwt-strategy
48 | // using the database strategy is preferred for production
49 | async jwt({ token, account, session }) {
50 | if (token.error)
51 | delete token.error
52 | if (account) {
53 | // First-time login, save the `access_token`, its expiry and the `refresh_token`
54 | return {
55 | // there's a limit of 4096 bytes for the JWT payload, so we only store the necessary data
56 | // use the database strategy for production..
57 | ...token,
58 | access_token: account.access_token,
59 | expires_at: account.expires_at,
60 | refresh_token: account.refresh_token,
61 | id_token: account.id_token ? Buffer.from(account.id_token.split(".")[1], "base64").toString() : "",
62 | }
63 | } else if (!session?.refresh && token.expires_at && Date.now() < token.expires_at * 1000) {
64 | // Subsequent logins, but the `access_token` is still valid
65 | return token
66 | } else {
67 | // Subsequent logins, but the `access_token` has expired, try to refresh it
68 | if (!token.refresh_token) throw new TypeError("Missing refresh_token")
69 |
70 | try {
71 | // The `token_endpoint` can be found in the provider's documentation. Or if they support OIDC,
72 | // at their `/.well-known/openid-configuration` endpoint.
73 | const response = await fetch(baseUrl + "/oauth2/v1/apps/token", {
74 | method: "POST",
75 | body: new URLSearchParams({
76 | client_id: clientId,
77 | client_secret: clientSecret,
78 | grant_type: "refresh_token",
79 | refresh_token: token.refresh_token!,
80 | }),
81 | })
82 |
83 | const tokensOrError = await response.json()
84 |
85 | if (!response.ok) throw tokensOrError
86 |
87 | const newTokens = tokensOrError as {
88 | access_token: string
89 | expires_in: number
90 | refresh_token?: string
91 | id_token?: string
92 | }
93 |
94 | token.access_token = newTokens.access_token
95 | token.id_token = newTokens.id_token ? Buffer.from(newTokens.id_token.split(".")[1], "base64").toString() : "";
96 | token.expires_at = Math.floor(
97 | Date.now() / 1000 + newTokens.expires_in
98 | )
99 | // Some providers only issue refresh tokens once, so preserve if we did not get a new one
100 | if (newTokens.refresh_token)
101 | token.refresh_token = newTokens.refresh_token
102 |
103 | return token
104 | } catch (error) {
105 | console.error("Error refreshing access_token", error)
106 | // If we fail to refresh the token, return an error so we can handle it on the page
107 | token.error = "RefreshTokenError"
108 | return token
109 | }
110 | }
111 | },
112 | async session({ session, token }) {
113 | session.error = token.error
114 | session.user.id_token = token.id_token
115 | session.user.access_token = token.access_token
116 | session.user.refresh_token = token.refresh_token
117 | session.user.expires_at = token.expires_at
118 | return session
119 | },
120 | },
121 |
122 | });
123 |
124 |
125 | declare module "next-auth" {
126 | interface Session {
127 | error?: "RefreshTokenError"
128 | user: {
129 | id_token?: string
130 | access_token?: string
131 | expires_at?: number
132 | refresh_token?: string
133 | } & DefaultSession["user"]
134 | }
135 | }
136 |
137 | declare module "@auth/core/jwt" {
138 | interface JWT extends DefaultJWT {
139 | id_token?: string
140 | access_token?: string
141 | expires_at?: number
142 | refresh_token?: string
143 | error?: "RefreshTokenError"
144 | }
145 | }
--------------------------------------------------------------------------------