Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/backend/src/oauth/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { GoogleProvider } from "./providers/google";
import { LinkedInProvider } from "./providers/linkedin";
import { MicrosoftProvider } from "./providers/microsoft";
import { MockProvider } from "./providers/mock";
import { OktaProvider } from "./providers/okta";
import { SpotifyProvider } from "./providers/spotify";
import { TwitchProvider } from "./providers/twitch";
import { XProvider } from "./providers/x";
Expand All @@ -31,6 +32,7 @@ const _providers = {
linkedin: LinkedInProvider,
x: XProvider,
twitch: TwitchProvider,
okta: OktaProvider,
} as const;

const mockProvider = MockProvider;
Expand Down Expand Up @@ -78,6 +80,7 @@ export async function getProvider(provider: Tenancy['config']['auth']['oauth']['
clientSecret: provider.clientSecret || throwErr("Client secret is required for standard providers"),
facebookConfigId: provider.facebookConfigId,
microsoftTenantId: provider.microsoftTenantId,
oktaDomain: provider.oktaDomain,
});
}
}
Expand Down
79 changes: 79 additions & 0 deletions apps/backend/src/oauth/providers/okta.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env";
import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors";
import { getJwtInfo } from "@stackframe/stack-shared/dist/utils/jwt";
import { OAuthUserInfo, validateUserInfo } from "../utils";
import { OAuthBaseProvider, TokenSet } from "./base";


export class OktaProvider extends OAuthBaseProvider {
private oktaDomain : string;
private constructor(
oktaDomain: string,
...args: ConstructorParameters<typeof OAuthBaseProvider>
) {
super(...args);
this.oktaDomain = oktaDomain
}

static async create(options: {
clientId: string;
clientSecret: string;
oktaDomain: string;
}) {
const oktaDomain = options.oktaDomain;

if(!oktaDomain) throw new StackAssertionError("Okta domain is required ")

return new OktaProvider(
oktaDomain,
...await OAuthBaseProvider.createConstructorArgs({
issuer: `https://${oktaDomain}`,
authorizationEndpoint: `https://${oktaDomain}/v1/authorize`,
tokenEndpoint: `https://${oktaDomain}/v1/token`,
redirectUri: getEnvVariable("OAUTH_REDIRECT_URI")!,
jwksUri: `https://${oktaDomain}/v1/keys`,
baseScope: "openid email profile",
authorizationExtraParams: { response_mode: "form_post" },
tokenEndpointAuthMethod: "client_secret_basic",
...options,
}))
;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

async postProcessUserInfo(tokenSet: TokenSet): Promise<OAuthUserInfo> {
const rawUserInfoRes = await fetch(`https://${this.oktaDomain}/v1/userinfo`,{
headers: {
Authorization: `Bearer ${tokenSet.accessToken}`,
},
});

if (!rawUserInfoRes.ok) {
throw new StackAssertionError(
"Error fetching user information from Okta",
{
status: rawUserInfoRes.status,
body: await rawUserInfoRes.text(),
jwtInfo: await getJwtInfo({ jwt: tokenSet.accessToken }),
}
);
}

const rawUserInfo = await rawUserInfoRes.json();

return validateUserInfo({
accountId: rawUserInfo.sub,
displayName: rawUserInfo.name,
profileImageUrl: rawUserInfo.picture,
email: rawUserInfo.email,
emailVerified: rawUserInfo.email_verified,
});
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
async checkAccessTokenValidity(accessToken: string): Promise<boolean> {
const res = await fetch(`https://${this.oktaDomain}/v1/userinfo`,{
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
return res.ok;
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ function toTitle(id: string) {
linkedin: "LinkedIn",
twitch: "Twitch",
x: "X",
okta: "Okta"
}[id];
}

Expand All @@ -63,6 +64,7 @@ export const providerFormSchema = yupObject({
}),
facebookConfigId: yupString().optional(),
microsoftTenantId: yupString().optional(),
oktaDomain: yupString().optional()
});

export type ProviderFormValues = yup.InferType<typeof providerFormSchema>
Expand All @@ -75,6 +77,7 @@ export function ProviderSettingDialog(props: Props & { open: boolean, onClose: (
clientSecret: (props.provider as any)?.clientSecret ?? "",
facebookConfigId: (props.provider as any)?.facebookConfigId ?? "",
microsoftTenantId: (props.provider as any)?.microsoftTenantId ?? "",
oktaDomain:(props.provider as any)?.oktaDomain?? "",
};

const onSubmit = async (values: ProviderFormValues) => {
Expand All @@ -88,6 +91,7 @@ export function ProviderSettingDialog(props: Props & { open: boolean, onClose: (
clientSecret: values.clientSecret || "",
facebookConfigId: values.facebookConfigId,
microsoftTenantId: values.microsoftTenantId,
oktaDomain: values.oktaDomain,
});
}
};
Expand Down Expand Up @@ -166,6 +170,15 @@ export function ProviderSettingDialog(props: Props & { open: boolean, onClose: (
placeholder="Tenant ID"
/>
)}

{props.id === 'Okta' && (
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: case mismatch - should be 'okta' (lowercase) to match the provider key in toTitle function and backend

Suggested change
{props.id === 'Okta' && (
{props.id === 'okta' && (
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/auth-methods/providers.tsx
Line: 174:174

Comment:
**logic:** case mismatch - should be `'okta'` (lowercase) to match the provider key in `toTitle` function and backend

```suggestion
              {props.id === 'okta' && (
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
{props.id === 'Okta' && (
{props.id === 'okta' && (

The condition checks for props.id === 'Okta' (capitalized), but the provider ID throughout the codebase is lowercase 'okta'. This means the Okta domain input field will never be displayed in the UI.

View Details

Analysis

Okta domain input field never renders due to incorrect provider ID case

What fails: The Okta domain input field is never displayed in the OAuth provider configuration dialog, preventing users from entering the required Okta domain when setting up Okta OAuth authentication.

How to reproduce:

  1. Navigate to the dashboard OAuth provider settings for Okta
  2. Enable Okta OAuth and attempt to configure it with custom credentials (not shared keys)
  3. Observe that the "Okta Domain" input field is missing from the form

Root cause: The condition at line 174 in apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/auth-methods/providers.tsx checks props.id === 'Okta' (capitalized), but the provider ID throughout the codebase is lowercase 'okta' (defined in apps/backend/src/oauth/index.tsx line 35 as okta:OktaProvider, and used in the toTitle mapping at line 47 as okta:"Okta"). This string comparison never matches, so the field is never rendered.

Expected behavior: The Okta domain input field should be displayed when configuring Okta OAuth, consistent with how the facebook and microsoft provider conditions work (lines 155 and 164, which use lowercase IDs).

Fix: Changed line 174 from props.id === 'Okta' to props.id === 'okta' to match the actual provider ID used throughout the codebase.

<InputField
control={form.control}
name="oktaDomain"
label="Okta Domain (required if you are using Okta)"
placeholder="oktaDomain"
/>
)}
Comment on lines +174 to +181
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Case mismatch prevents Okta domain field from rendering.

The condition checks props.id === 'Okta' (capitalized), but provider IDs are lowercase throughout the codebase. Looking at line 47, the toTitle mapping uses lowercase keys (okta: "Okta"). The similar checks for Facebook (line 156) and Microsoft (line 165) both use lowercase IDs. This condition will always be false, preventing the Okta domain input field from ever being displayed, which breaks the Okta configuration UI.

Apply this diff:

-              {props.id === 'Okta' && (
+              {props.id === 'okta' && (
                <InputField
                  control={form.control}
                  name="oktaDomain"
                  label="Okta Domain (required if you are using Okta)"
                  placeholder="oktaDomain"
                />
              )}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{props.id === 'Okta' && (
<InputField
control={form.control}
name="oktaDomain"
label="Okta Domain (required if you are using Okta)"
placeholder="oktaDomain"
/>
)}
{props.id === 'okta' && (
<InputField
control={form.control}
name="oktaDomain"
label="Okta Domain (required if you are using Okta)"
placeholder="oktaDomain"
/>
)}
🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/auth-methods/providers.tsx
around lines 174 to 181, the conditional rendering check uses props.id ===
'Okta' (capitalized) which never matches the lowercase provider IDs used
elsewhere; change the comparison to props.id === 'okta' to match the existing id
keys (consistent with the toTitle mapping and other provider checks) so the Okta
Domain InputField is rendered correctly.

</>
)}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -375,8 +375,7 @@ it("returns an error when the oauth config is misconfigured", async ({ expect })
expect(invalidTypeResponse).toMatchInlineSnapshot(`
NiceResponse {
"status": 400,
"body": "auth.oauth.providers.invalid.type must be one of the following values: google, github, microsoft, spotify, facebook, discord, gitlab, bitbucket, linkedin, apple, x, twitch",
"headers": Headers { <some fields may have been hidden> },
"body": "auth.oauth.providers.invalid.type must be one of the following values: google, github, microsoft, spotify, facebook, discord, gitlab, bitbucket, linkedin, apple, x, twitch, okta",
}
`);
});
Expand Down