Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: thomask33/07-12-feat_oauth2_add_client_credentials_grant_type_and_user_ownership
Choose a base branch
from

Conversation

ThomasK33
Copy link
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.

Copy link
Member Author

ThomasK33 commented Jul 14, 2025

@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
Collaborator

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

Copy link
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
Collaborator

@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?

@@ -1726,11 +1726,18 @@ class ApiMethods {
getOAuth2ProviderApps = async (
filter?: TypesGen.OAuth2ProviderAppFilter,
Copy link
Collaborator

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;
};

@@ -21,10 +21,10 @@ export const getGitHubDeviceFlowCallback = (code: string, state: string) => {
};
};

export const getApps = (userId?: string) => {
export const getApps = (filter?: { user_id?: string; owner_id?: string }) => {
Copy link
Collaborator

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),
	};
};

@@ -1606,17 +1606,22 @@ export interface OAuth2ProviderApp {
readonly name: string;
readonly redirect_uris: readonly string[];
readonly icon: string;
readonly created_at: string;
readonly grant_types: readonly string[];
readonly user_id?: string;
Copy link
Collaborator

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
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
Collaborator

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
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
Collaborator

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
Collaborator

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
Collaborator

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
Collaborator

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
Collaborator

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
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
Collaborator

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
Collaborator

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
Collaborator

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.

@@ -61,7 +61,7 @@ export const Sidebar: FC<SidebarProps> = ({ user }) => {
<SidebarNavItem href="ssh-keys" icon={FingerprintIcon}>
SSH Keys
</SidebarNavItem>
<SidebarNavItem href="tokens" icon={KeyIcon}>
<SidebarNavItem href="tokens" icon={KeyIcon} end={false}>
Copy link
Collaborator

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
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
Collaborator

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.

Valid: true,
})
if err != nil {
httpapi.InternalServerError(rw, err)
Copy link
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
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
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

- 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 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
Collaborator

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants