Skip to content

feat: add client credentials OAuth2 applications for API access#18846

Closed
ThomasK33 wants to merge 1 commit into
thomask33/07-12-feat_oauth2_add_client_credentials_grant_type_and_user_ownershipfrom
thomask33/07-14-feat_oauth2_add_frontend_ui_for_client_credentials_applications
Closed

feat: add client credentials OAuth2 applications for API access#18846
ThomasK33 wants to merge 1 commit into
thomask33/07-12-feat_oauth2_add_client_credentials_grant_type_and_user_ownershipfrom
thomask33/07-14-feat_oauth2_add_frontend_ui_for_client_credentials_applications

Conversation

@ThomasK33
Copy link
Copy Markdown
Member

Add OAuth2 Client Credentials Grant Type Support

This PR adds support for OAuth2 client credentials grant type, enabling server-to-server API access. Key changes include:

  • Added new fields to OAuth2ProviderApp model: created_at, grant_types, and user_id
  • Added created_at field to OAuth2ProviderAppSecret model
  • Added revocation endpoint to OAuth2AppEndpoints
  • Created new UI components for managing client credentials applications:
    • ClientCredentialsAppForm for creating/editing applications
    • ClientCredentialsAppRow for displaying applications in a table
  • Added new routes for client credentials applications:
    • /settings/oauth2-provider/new for creating new applications
    • /settings/oauth2-provider/:appId for managing existing applications
  • Redesigned OAuth2 applications settings page to separate owned applications from authorized applications
  • Enhanced secret creation dialog with code examples for API usage
  • Updated API documentation to reflect new fields and endpoints

This implementation provides a more secure alternative to long-lived API tokens by allowing users to create OAuth2 applications with client credentials grant type for server-to-server authentication.

@ThomasK33 ThomasK33 force-pushed the thomask33/07-12-feat_oauth2_add_client_credentials_grant_type_and_user_ownership branch from c654948 to eac2681 Compare July 14, 2025 16:22
@ThomasK33 ThomasK33 force-pushed the thomask33/07-14-feat_oauth2_add_frontend_ui_for_client_credentials_applications branch from 8c29819 to 168176b Compare July 14, 2025 16:22
@ThomasK33 ThomasK33 force-pushed the thomask33/07-12-feat_oauth2_add_client_credentials_grant_type_and_user_ownership branch from eac2681 to ac0d1f6 Compare July 14, 2025 17:18
@ThomasK33 ThomasK33 force-pushed the thomask33/07-14-feat_oauth2_add_frontend_ui_for_client_credentials_applications branch 2 times, most recently from 4fcf5b1 to 65b1054 Compare July 14, 2025 17:46
@ThomasK33 ThomasK33 marked this pull request as ready for review July 14, 2025 18:09
@BrunoQuaresma
Copy link
Copy Markdown
Contributor

I'm doing the review, but I'm really missing a video demoing the feature, or at least screenshots.

Copy link
Copy Markdown
Member Author

I'm doing the review, but I'm really missing a video demoing the feature, or at least screenshots.

I’ll add some screenshots tomorrow

Copy link
Copy Markdown
Contributor

@BrunoQuaresma BrunoQuaresma left a comment

Choose a reason for hiding this comment

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

Thanks for working on the FE 🙏 👏

Since there are a good amount of changes, and I want to be sure I'm going to review them properly, and not overwhelming you, I think it would make sense to run a few review rounds.

For the first round, I pointed one blocker, and a few major improvements we can do. After we work on that, I will run another review and see if there is any minor left.

Sounds good?

Comment thread site/src/api/api.ts Outdated
@@ -1726,11 +1726,18 @@ class ApiMethods {
getOAuth2ProviderApps = async (
filter?: TypesGen.OAuth2ProviderAppFilter,
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.

I think we can simplify this function by just making the filter not optional and adding a default value as {} so we could use it directly in the URLSearchParams without having to do many ifs.

getOAuth2ProviderApps = async (
	filter: TypesGen.OAuth2ProviderAppFilter = {},
): Promise<TypesGen.OAuth2ProviderApp[]> => {
	const params = new URLSearchParams(filter);
	const resp = await this.axios.get(
		"/api/v2/oauth2-provider/apps", { params },
	);
	return resp.data;
};

Comment thread site/src/api/queries/oauth2.ts Outdated
};

export const getApps = (userId?: string) => {
export const getApps = (filter?: { user_id?: string; owner_id?: string }) => {
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.

Another function we can improve.

export const getApps = (filter: OAuth2ProviderAppFilter = {}) => {
	return {
		queryKey: [...appsKey, filter],
		queryFn: () => API.getOAuth2ProviderApps(filter),
	};
};

readonly icon: string;
readonly created_at: string;
readonly grant_types: readonly string[];
readonly user_id?: string;
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.

Is it possible to have an OAuth2ProviderApp without an user?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

yes, that's possible

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.

What is the use case?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Different flows/grant types. Client credential flows have to be owned/tied to a user, while authorization code flow must not be tied to a user.

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.

Do you think would make sense to left a comment in this attribute, in the codersdk type? I think this is something I would easily forget in the future.

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.

My overall feeling is this component is doing way more than a form should do. I think we can improve by:

  • Remove props that are not going to change and use the value directly
  • Execute the mutation in the form, and only call the onSubmit or onSubmitSuccess if the parent needs that

By applying these, I think we can significantly reduce the complexity of this component. This may require a few changes, so probably will need a second review round.

event.preventDefault();
const formData = new FormData(event.target as HTMLFormElement);

onSubmit({
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.

I know using the WEB API is nice, but it has some gaps and to make the code consistent lets use Formik and Yup for validation.

You can easily find examples on how to use both in the codebase.

const [showCodeExample, setShowCodeExample] = useState(false);

// Fetch users and find the owner by user_id
const usersQuery = useQuery({
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.

⚠️ Blocker

The BE supports filtering a user by username or email, if that is not available in the app, I think we can do.

  • Add the username or user email to the app response
  • Add an endpoint into the backend to fetch user data by ID

I don't think we should move forward with the current solution.

hideCancel
{fullNewSecret && app && (
<Dialog
css={{
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.

We should avoid adding a new dialog style only for this case. Did you take a look in the other dialogs? Wondering what you want to achieve here that is not achievable by using the existent dialogs.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I can refactor that into a component if you'd like, but none of the other dialogs allow resizing since the max width is always hardcoded.

When opening the code example in the dialog, I want it to expand both horizontally and vertically so that the word wrap on the curl command in the code example doesn't apply, and users can see the command they are about to copy and paste without having first to copy it and then inspect it in another editor.

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.

Ok, lets keep it as it is - in terms of design. I will give it a try and see what we can do to better support this use case in our design system.

OAuth2 client secret
</h3>
<div
css={{
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.

We are not using inline CSS styles anymore. Give a try to TailwindCSS class names. You can see many of them in the code base.

}
/>
<div css={{ marginTop: 24 }}>
<Link
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.

Looks like it should be a button instead of a link. Links should be used for navigation.

SSH Keys
</SidebarNavItem>
<SidebarNavItem href="tokens" icon={KeyIcon}>
<SidebarNavItem href="tokens" icon={KeyIcon} end={false}>
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.

Wondering why only these two have end set to false 🤔

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

None of the other items has nested menus. Only the tokens and OAuth2 app section can navigate to another route, thus making the nav item "unselected".

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.

I will give this a try and see how it behaves.

Comment thread coderd/database/db2sdk/db2sdk.go Outdated
Comment thread coderd/oauth2provider/apps.go Outdated
Valid: true,
})
if err != nil {
httpapi.InternalServerError(rw, err)
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.

Let's also add a log here; we probably don't want to leak the error details back in the HTTP response, so let's just say something generic; admins can refer to the logs for details.

Copy link
Copy Markdown
Contributor

@dannykopping dannykopping Jul 16, 2025

Choose a reason for hiding this comment

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

You're still returning the error directly; we probably don't want to do that; ditto for the other cases.

@ThomasK33 ThomasK33 force-pushed the thomask33/07-14-feat_oauth2_add_frontend_ui_for_client_credentials_applications branch from 65b1054 to f044533 Compare July 15, 2025 17:27
@ThomasK33 ThomasK33 force-pushed the thomask33/07-12-feat_oauth2_add_client_credentials_grant_type_and_user_ownership branch from ac0d1f6 to ef3c66e Compare July 15, 2025 17:27
@ThomasK33 ThomasK33 force-pushed the thomask33/07-14-feat_oauth2_add_frontend_ui_for_client_credentials_applications branch 2 times, most recently from 5f71ebd to c84c4be Compare July 17, 2025 13:30
@ThomasK33
Copy link
Copy Markdown
Member Author

ThomasK33 commented Jul 17, 2025

@BrunoQuaresma, sorry it took me a while, but here's a video showing off the client_credentials UI, including the expanding dialog:

coder_client_credentials_ui.mp4

@ThomasK33 ThomasK33 force-pushed the thomask33/07-12-feat_oauth2_add_client_credentials_grant_type_and_user_ownership branch from ef3c66e to 3f1495c Compare July 17, 2025 14:38
@ThomasK33 ThomasK33 requested a review from aslilac as a code owner July 17, 2025 14:38
@ThomasK33 ThomasK33 force-pushed the thomask33/07-14-feat_oauth2_add_frontend_ui_for_client_credentials_applications branch from c84c4be to 40d7fd1 Compare July 17, 2025 14:38
@ThomasK33 ThomasK33 requested a review from BrunoQuaresma July 17, 2025 14:50
Copy link
Copy Markdown
Contributor

@BrunoQuaresma BrunoQuaresma left a comment

Choose a reason for hiding this comment

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

Thanks for working in all my review comments. I think it is good to go 👍

PS: I think the UX is ok and there are a few improvements we can do. Ideally, we would have a design phase when adding or changing features.

@github-actions github-actions Bot closed this Aug 3, 2025
@ThomasK33 ThomasK33 assigned Emyrk and unassigned ThomasK33 Aug 12, 2025
@ThomasK33 ThomasK33 removed the stale This issue is like stale bread. label Aug 12, 2025
@ThomasK33 ThomasK33 reopened this Aug 12, 2025
@ThomasK33 ThomasK33 force-pushed the thomask33/07-14-feat_oauth2_add_frontend_ui_for_client_credentials_applications branch from cc78865 to 4ce585e Compare August 12, 2025 16:33
@github-actions github-actions Bot added the stale This issue is like stale bread. label Aug 20, 2025
@github-actions github-actions Bot closed this Aug 24, 2025
@Emyrk Emyrk reopened this Aug 24, 2025
@github-actions github-actions Bot removed the stale This issue is like stale bread. label Aug 25, 2025
@github-actions github-actions Bot added the stale This issue is like stale bread. label Sep 1, 2025
@github-actions github-actions Bot closed this Sep 5, 2025
@ThomasK33 ThomasK33 deleted the thomask33/07-14-feat_oauth2_add_frontend_ui_for_client_credentials_applications branch September 15, 2025 07:52
@ThomasK33 ThomasK33 restored the thomask33/07-14-feat_oauth2_add_frontend_ui_for_client_credentials_applications branch September 15, 2025 07:53
@Emyrk Emyrk reopened this Oct 6, 2025
Comment thread Makefile Outdated

test:
$(GIT_FLAGS) gotestsum --format standard-quiet $(GOTESTSUM_RETRY_FLAGS) --packages="./..." -- -v -short -count=1 $(if $(RUN),-run $(RUN))
$(GIT_FLAGS) gotestsum --format standard-quiet $(GOTESTSUM_RETRY_FLAGS) --packages="$(if $(PACKAGE),$(PACKAGE),./...)" -- -v -short -count=1 $(if $(RUN),-run $(RUN))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why do we need this?

@ThomasK33 ThomasK33 force-pushed the thomask33/07-14-feat_oauth2_add_frontend_ui_for_client_credentials_applications branch from 4ce585e to fd42ee1 Compare October 10, 2025 14:42
@ThomasK33 ThomasK33 force-pushed the thomask33/07-12-feat_oauth2_add_client_credentials_grant_type_and_user_ownership branch 2 times, most recently from 487fa65 to c5bbc6e Compare October 10, 2025 14:53
@ThomasK33 ThomasK33 force-pushed the thomask33/07-14-feat_oauth2_add_frontend_ui_for_client_credentials_applications branch from fd42ee1 to 65adff3 Compare October 10, 2025 14:53
- Add ClientCredentialsAppForm and ClientCredentialsAppRow components
- Update API schemas to include created_at, grant_types, and user_id fields
- Add dedicated pages for creating and managing client credentials apps
- Update sidebar navigation and routing for OAuth2 client credentials
- Enhance OAuth2AppPageView with user ownership information display

Change-Id: I3271c7fb995d7225dd6cc830066fa2c8cb29720a
Signed-off-by: Thomas Kosiewski <tk@coder.com>
@ThomasK33 ThomasK33 force-pushed the thomask33/07-12-feat_oauth2_add_client_credentials_grant_type_and_user_ownership branch from c5bbc6e to c659bbc Compare October 10, 2025 14:57
@ThomasK33 ThomasK33 force-pushed the thomask33/07-14-feat_oauth2_add_frontend_ui_for_client_credentials_applications branch from 65adff3 to 7d8adca Compare October 10, 2025 14:57
Copy link
Copy Markdown
Contributor

@buenos-nachos buenos-nachos left a comment

Choose a reason for hiding this comment

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

Did a quick scan since I figured that this PR has had a decent amount of review already. My main concern was around some of the conditional rendering logic

Comment thread site/src/api/api.ts
: "";

const resp = await this.axios.get(`/api/v2/oauth2-provider/apps?${params}`);
const params = new URLSearchParams(filter as Record<string, string>);
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.

Nit: while it shouldn't be an issue now, I'd prefer it if we avoid the type assertion, since this will silence any type mismatches in the future. How confident are we that we won't need to support non-strings values in the future?

I feel like the Go way of doing things makes sense here: copy the values from the filter into a new object that's passed directly to the constructor:

new URLSearchParams({
  user_id: filter.user_id ?? "",
  owner_id: filter.owner_id ?? ""
});

</Button>
</Stack>

<form className="mt-2.5" onSubmit={form.handleSubmit}>
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.

Nit: mt-2.5 can be swapped for pt-2.5, and should be less likely to cause layout side effects from margin collapse

<div className="font-medium mb-1">
About Client Credentials Applications
</div>
<div className="text-sm">
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.

This long-form text should be a p tag

Comment on lines +46 to +49
if (!appId) {
navigate("/settings/oauth2-provider");
return null;
}
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.

Two big problems here:

  1. navigate is a side effect function, and shouldn't be called from inside a render like this.
  2. The conditional rendering breaks React's rules of hooks. In practice, I don't see this code breaking, because the app ID shouldn't be able to change without a full page change, but breaking this rule in general can cause a React component to completely blow up and trigger our global error boundary. I don't feel comfortable with having code that the React team actively pushes you away from, because the justification here feels like "just trust me bro"

Two changes we can do:

  1. Add a useEffect call to handle the navigation
    useEffect(() => {
      if (!appId) {
        navigate("/settings/oauth2-provider");
      }
    }, [navigate, appId]);
  2. Anywhere after all the hooks mount, do conditional rendering that instead returns out the Navigate component (which is basically a wrapper over useNavigate+useEffect under the hood)

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.

React Router has a weird policy of not stabilizing their function references from their hooks, so if throwing navigate in the dependency array causes too many renders (or worse, infinite renders), you can do this instead:

const stableNavigate = useEffectEvent(navigate);
useEffect(() => {
  if (!appId) {
    stableNavigate("/settings/oauth2-provider");
  }
}, [stableNavigate, appId]);

Comment on lines +69 to +74
const handleUpdateApp = async (data: {
name: string;
icon: string;
grant_types: TypesGen.OAuth2ProviderGrantType[];
redirect_uris: string[];
}) => {
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.

This type definition is used in a few different places, and it's complicated enough that I feel like it'd be better to extract it into a custom type alias

const appToDelete = ownedApps.find((app) => app.id === appIdToDelete);

const handleManageOwnedApp = (app: TypesGen.OAuth2ProviderApp) => {
navigate(`${app.id}`);
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.

Is there a reason for the template literal?

@github-actions github-actions Bot removed the stale This issue is like stale bread. label Apr 9, 2026
@github-actions github-actions Bot added the stale This issue is like stale bread. label Apr 30, 2026
@github-actions github-actions Bot deleted the branch thomask33/07-12-feat_oauth2_add_client_credentials_grant_type_and_user_ownership May 2, 2026 00:23
@github-actions github-actions Bot closed this May 9, 2026
@github-actions github-actions Bot deleted the thomask33/07-14-feat_oauth2_add_frontend_ui_for_client_credentials_applications branch May 9, 2026 00:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stale This issue is like stale bread.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants