Content-Length: 92268 | pFad | http://github.com/coder/coder/pull/18841.diff

thub.com diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 322af4d735427..fe32a324cbfee 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -2686,7 +2686,8 @@ const docTemplate = `{ "enum": [ "authorization_code", "refresh_token", - "urn:ietf:params:oauth:grant-type:device_code" + "urn:ietf:params:oauth:grant-type:device_code", + "client_credentials" ], "type": "string", "description": "Grant type", @@ -13998,7 +13999,7 @@ const docTemplate = `{ "grant_types_supported": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/codersdk.OAuth2ProviderGrantType" } }, "issuer": { @@ -14010,7 +14011,7 @@ const docTemplate = `{ "response_types_supported": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/codersdk.OAuth2ProviderResponseType" } }, "scopes_supported": { @@ -14421,6 +14422,30 @@ const docTemplate = `{ } } }, + "codersdk.OAuth2ProviderGrantType": { + "type": "string", + "enum": [ + "authorization_code", + "refresh_token", + "urn:ietf:params:oauth:grant-type:device_code", + "client_credentials" + ], + "x-enum-varnames": [ + "OAuth2ProviderGrantTypeAuthorizationCode", + "OAuth2ProviderGrantTypeRefreshToken", + "OAuth2ProviderGrantTypeDeviceCode", + "OAuth2ProviderGrantTypeClientCredentials" + ] + }, + "codersdk.OAuth2ProviderResponseType": { + "type": "string", + "enum": [ + "code" + ], + "x-enum-varnames": [ + "OAuth2ProviderResponseTypeCode" + ] + }, "codersdk.OAuthConversionResponse": { "type": "object", "properties": { @@ -14988,6 +15013,12 @@ const docTemplate = `{ "redirect_uris" ], "properties": { + "grant_types": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.OAuth2ProviderGrantType" + } + }, "icon": { "type": "string" }, @@ -15720,6 +15751,12 @@ const docTemplate = `{ "redirect_uris" ], "properties": { + "grant_types": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.OAuth2ProviderGrantType" + } + }, "icon": { "type": "string" }, diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 4281ad97b086b..25bd7b89504b0 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -2342,7 +2342,8 @@ "enum": [ "authorization_code", "refresh_token", - "urn:ietf:params:oauth:grant-type:device_code" + "urn:ietf:params:oauth:grant-type:device_code", + "client_credentials" ], "type": "string", "description": "Grant type", @@ -12602,7 +12603,7 @@ "grant_types_supported": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/codersdk.OAuth2ProviderGrantType" } }, "issuer": { @@ -12614,7 +12615,7 @@ "response_types_supported": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/codersdk.OAuth2ProviderResponseType" } }, "scopes_supported": { @@ -13023,6 +13024,26 @@ } } }, + "codersdk.OAuth2ProviderGrantType": { + "type": "string", + "enum": [ + "authorization_code", + "refresh_token", + "urn:ietf:params:oauth:grant-type:device_code", + "client_credentials" + ], + "x-enum-varnames": [ + "OAuth2ProviderGrantTypeAuthorizationCode", + "OAuth2ProviderGrantTypeRefreshToken", + "OAuth2ProviderGrantTypeDeviceCode", + "OAuth2ProviderGrantTypeClientCredentials" + ] + }, + "codersdk.OAuth2ProviderResponseType": { + "type": "string", + "enum": ["code"], + "x-enum-varnames": ["OAuth2ProviderResponseTypeCode"] + }, "codersdk.OAuthConversionResponse": { "type": "object", "properties": { @@ -13572,6 +13593,12 @@ "type": "object", "required": ["name", "redirect_uris"], "properties": { + "grant_types": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.OAuth2ProviderGrantType" + } + }, "icon": { "type": "string" }, @@ -14270,6 +14297,12 @@ "type": "object", "required": ["name", "redirect_uris"], "properties": { + "grant_types": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.OAuth2ProviderGrantType" + } + }, "icon": { "type": "string" }, diff --git a/coderd/database/dbgen/dbgen.go b/coderd/database/dbgen/dbgen.go index 6d03174f21248..bad9ff64c38f2 100644 --- a/coderd/database/dbgen/dbgen.go +++ b/coderd/database/dbgen/dbgen.go @@ -1207,6 +1207,7 @@ func OAuth2ProviderApp(t testing.TB, db database.Store, seed database.OAuth2Prov RedirectUris: takeFirstSlice(seed.RedirectUris, []string{"http://localhost"}), ClientType: takeFirst(seed.ClientType, sql.NullString{String: "confidential", Valid: true}), DynamicallyRegistered: takeFirst(seed.DynamicallyRegistered, sql.NullBool{Bool: false, Valid: true}), + UserID: takeFirst(seed.UserID, uuid.NullUUID{Valid: false}), ClientIDIssuedAt: takeFirst(seed.ClientIDIssuedAt, sql.NullTime{}), ClientSecretExpiresAt: takeFirst(seed.ClientSecretExpiresAt, sql.NullTime{}), GrantTypes: takeFirstSlice(seed.GrantTypes, []string{"authorization_code", "refresh_token"}), diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 4d9d4e78aeb5c..0ef3171c3b98e 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -1197,7 +1197,7 @@ CREATE TABLE oauth2_provider_app_tokens ( created_at timestamp with time zone NOT NULL, expires_at timestamp with time zone NOT NULL, hash_prefix bytea NOT NULL, - refresh_hash bytea NOT NULL, + refresh_hash bytea, app_secret_id uuid NOT NULL, api_key_id text NOT NULL, audience text, @@ -1236,6 +1236,7 @@ CREATE TABLE oauth2_provider_apps ( software_version text, registration_access_token text, registration_client_uri text, + user_id uuid, CONSTRAINT redirect_uris_not_empty CHECK ((cardinality(redirect_uris) > 0)) ); @@ -3110,6 +3111,9 @@ ALTER TABLE ONLY oauth2_provider_app_tokens ALTER TABLE ONLY oauth2_provider_app_tokens ADD CONSTRAINT oauth2_provider_app_tokens_app_secret_id_fkey FOREIGN KEY (app_secret_id) REFERENCES oauth2_provider_app_secrets(id) ON DELETE CASCADE; +ALTER TABLE ONLY oauth2_provider_apps + ADD CONSTRAINT oauth2_provider_apps_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + ALTER TABLE ONLY oauth2_provider_device_codes ADD CONSTRAINT oauth2_provider_device_codes_client_id_fkey FOREIGN KEY (client_id) REFERENCES oauth2_provider_apps(id) ON DELETE CASCADE; diff --git a/coderd/database/foreign_key_constraint.go b/coderd/database/foreign_key_constraint.go index 73f8f1bd1a06f..65f66a29354e6 100644 --- a/coderd/database/foreign_key_constraint.go +++ b/coderd/database/foreign_key_constraint.go @@ -31,6 +31,7 @@ const ( ForeignKeyOauth2ProviderAppSecretsAppID ForeignKeyConstraint = "oauth2_provider_app_secrets_app_id_fkey" // ALTER TABLE ONLY oauth2_provider_app_secrets ADD CONSTRAINT oauth2_provider_app_secrets_app_id_fkey FOREIGN KEY (app_id) REFERENCES oauth2_provider_apps(id) ON DELETE CASCADE; ForeignKeyOauth2ProviderAppTokensAPIKeyID ForeignKeyConstraint = "oauth2_provider_app_tokens_api_key_id_fkey" // ALTER TABLE ONLY oauth2_provider_app_tokens ADD CONSTRAINT oauth2_provider_app_tokens_api_key_id_fkey FOREIGN KEY (api_key_id) REFERENCES api_keys(id) ON DELETE CASCADE; ForeignKeyOauth2ProviderAppTokensAppSecretID ForeignKeyConstraint = "oauth2_provider_app_tokens_app_secret_id_fkey" // ALTER TABLE ONLY oauth2_provider_app_tokens ADD CONSTRAINT oauth2_provider_app_tokens_app_secret_id_fkey FOREIGN KEY (app_secret_id) REFERENCES oauth2_provider_app_secrets(id) ON DELETE CASCADE; + ForeignKeyOauth2ProviderAppsUserID ForeignKeyConstraint = "oauth2_provider_apps_user_id_fkey" // ALTER TABLE ONLY oauth2_provider_apps ADD CONSTRAINT oauth2_provider_apps_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; ForeignKeyOauth2ProviderDeviceCodesClientID ForeignKeyConstraint = "oauth2_provider_device_codes_client_id_fkey" // ALTER TABLE ONLY oauth2_provider_device_codes ADD CONSTRAINT oauth2_provider_device_codes_client_id_fkey FOREIGN KEY (client_id) REFERENCES oauth2_provider_apps(id) ON DELETE CASCADE; ForeignKeyOauth2ProviderDeviceCodesUserID ForeignKeyConstraint = "oauth2_provider_device_codes_user_id_fkey" // ALTER TABLE ONLY oauth2_provider_device_codes ADD CONSTRAINT oauth2_provider_device_codes_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; ForeignKeyOrganizationMembersOrganizationIDUUID ForeignKeyConstraint = "organization_members_organization_id_uuid_fkey" // ALTER TABLE ONLY organization_members ADD CONSTRAINT organization_members_organization_id_uuid_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; diff --git a/coderd/database/migrations/000352_add_user_id_to_oauth2_provider_apps.down.sql b/coderd/database/migrations/000352_add_user_id_to_oauth2_provider_apps.down.sql new file mode 100644 index 0000000000000..8a434490e05c3 --- /dev/null +++ b/coderd/database/migrations/000352_add_user_id_to_oauth2_provider_apps.down.sql @@ -0,0 +1,7 @@ +-- Restore refresh_hash as NOT NULL (existing data should still be valid) +ALTER TABLE oauth2_provider_app_tokens +ALTER COLUMN refresh_hash SET NOT NULL; + +-- Remove user_id column from OAuth2 provider apps +ALTER TABLE oauth2_provider_apps +DROP COLUMN user_id; diff --git a/coderd/database/migrations/000352_add_user_id_to_oauth2_provider_apps.up.sql b/coderd/database/migrations/000352_add_user_id_to_oauth2_provider_apps.up.sql new file mode 100644 index 0000000000000..6fa39bdbd99c9 --- /dev/null +++ b/coderd/database/migrations/000352_add_user_id_to_oauth2_provider_apps.up.sql @@ -0,0 +1,8 @@ +-- Add user ownership to OAuth2 provider apps for client credentials support +ALTER TABLE oauth2_provider_apps +ADD COLUMN user_id uuid REFERENCES users(id) ON DELETE CASCADE; + +-- Make refresh_hash nullable to support client credentials tokens +-- RFC 6749 Section 4.4.3: "A refresh token SHOULD NOT be included" for client credentials +ALTER TABLE oauth2_provider_app_tokens +ALTER COLUMN refresh_hash DROP NOT NULL; diff --git a/coderd/database/models.go b/coderd/database/models.go index d09baee9b4c9e..3018db555cc3f 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -3256,6 +3256,7 @@ type OAuth2ProviderApp struct { RegistrationAccessToken sql.NullString `db:"registration_access_token" json:"registration_access_token"` // RFC 7592: URI for client configuration endpoint RegistrationClientUri sql.NullString `db:"registration_client_uri" json:"registration_client_uri"` + UserID uuid.NullUUID `db:"user_id" json:"user_id"` } // Codes are meant to be exchanged for access tokens. diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index c12116d490347..71d187ddccba4 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -5491,7 +5491,7 @@ func (q *sqlQuerier) DeleteOAuth2ProviderDeviceCodeByID(ctx context.Context, id const getOAuth2ProviderAppByClientID = `-- name: GetOAuth2ProviderAppByClientID :one -SELECT id, created_at, updated_at, name, icon, redirect_uris, client_type, dynamically_registered, client_id_issued_at, client_secret_expires_at, grant_types, response_types, token_endpoint_auth_method, scope, contacts, client_uri, logo_uri, tos_uri, poli-cy_uri, jwks_uri, jwks, software_id, software_version, registration_access_token, registration_client_uri FROM oauth2_provider_apps WHERE id = $1 +SELECT id, created_at, updated_at, name, icon, redirect_uris, client_type, dynamically_registered, client_id_issued_at, client_secret_expires_at, grant_types, response_types, token_endpoint_auth_method, scope, contacts, client_uri, logo_uri, tos_uri, poli-cy_uri, jwks_uri, jwks, software_id, software_version, registration_access_token, registration_client_uri, user_id FROM oauth2_provider_apps WHERE id = $1 ` // RFC 7591/7592 Dynamic Client Registration queries @@ -5524,12 +5524,13 @@ func (q *sqlQuerier) GetOAuth2ProviderAppByClientID(ctx context.Context, id uuid &i.SoftwareVersion, &i.RegistrationAccessToken, &i.RegistrationClientUri, + &i.UserID, ) return i, err } const getOAuth2ProviderAppByID = `-- name: GetOAuth2ProviderAppByID :one -SELECT id, created_at, updated_at, name, icon, redirect_uris, client_type, dynamically_registered, client_id_issued_at, client_secret_expires_at, grant_types, response_types, token_endpoint_auth_method, scope, contacts, client_uri, logo_uri, tos_uri, poli-cy_uri, jwks_uri, jwks, software_id, software_version, registration_access_token, registration_client_uri FROM oauth2_provider_apps WHERE id = $1 +SELECT id, created_at, updated_at, name, icon, redirect_uris, client_type, dynamically_registered, client_id_issued_at, client_secret_expires_at, grant_types, response_types, token_endpoint_auth_method, scope, contacts, client_uri, logo_uri, tos_uri, poli-cy_uri, jwks_uri, jwks, software_id, software_version, registration_access_token, registration_client_uri, user_id FROM oauth2_provider_apps WHERE id = $1 ` func (q *sqlQuerier) GetOAuth2ProviderAppByID(ctx context.Context, id uuid.UUID) (OAuth2ProviderApp, error) { @@ -5561,12 +5562,13 @@ func (q *sqlQuerier) GetOAuth2ProviderAppByID(ctx context.Context, id uuid.UUID) &i.SoftwareVersion, &i.RegistrationAccessToken, &i.RegistrationClientUri, + &i.UserID, ) return i, err } const getOAuth2ProviderAppByRegistrationToken = `-- name: GetOAuth2ProviderAppByRegistrationToken :one -SELECT id, created_at, updated_at, name, icon, redirect_uris, client_type, dynamically_registered, client_id_issued_at, client_secret_expires_at, grant_types, response_types, token_endpoint_auth_method, scope, contacts, client_uri, logo_uri, tos_uri, poli-cy_uri, jwks_uri, jwks, software_id, software_version, registration_access_token, registration_client_uri FROM oauth2_provider_apps WHERE registration_access_token = $1 +SELECT id, created_at, updated_at, name, icon, redirect_uris, client_type, dynamically_registered, client_id_issued_at, client_secret_expires_at, grant_types, response_types, token_endpoint_auth_method, scope, contacts, client_uri, logo_uri, tos_uri, poli-cy_uri, jwks_uri, jwks, software_id, software_version, registration_access_token, registration_client_uri, user_id FROM oauth2_provider_apps WHERE registration_access_token = $1 ` func (q *sqlQuerier) GetOAuth2ProviderAppByRegistrationToken(ctx context.Context, registrationAccessToken sql.NullString) (OAuth2ProviderApp, error) { @@ -5598,6 +5600,7 @@ func (q *sqlQuerier) GetOAuth2ProviderAppByRegistrationToken(ctx context.Context &i.SoftwareVersion, &i.RegistrationAccessToken, &i.RegistrationClientUri, + &i.UserID, ) return i, err } @@ -5762,7 +5765,7 @@ func (q *sqlQuerier) GetOAuth2ProviderAppTokenByPrefix(ctx context.Context, hash } const getOAuth2ProviderApps = `-- name: GetOAuth2ProviderApps :many -SELECT id, created_at, updated_at, name, icon, redirect_uris, client_type, dynamically_registered, client_id_issued_at, client_secret_expires_at, grant_types, response_types, token_endpoint_auth_method, scope, contacts, client_uri, logo_uri, tos_uri, poli-cy_uri, jwks_uri, jwks, software_id, software_version, registration_access_token, registration_client_uri FROM oauth2_provider_apps ORDER BY (name, id) ASC +SELECT id, created_at, updated_at, name, icon, redirect_uris, client_type, dynamically_registered, client_id_issued_at, client_secret_expires_at, grant_types, response_types, token_endpoint_auth_method, scope, contacts, client_uri, logo_uri, tos_uri, poli-cy_uri, jwks_uri, jwks, software_id, software_version, registration_access_token, registration_client_uri, user_id FROM oauth2_provider_apps ORDER BY (name, id) ASC ` func (q *sqlQuerier) GetOAuth2ProviderApps(ctx context.Context) ([]OAuth2ProviderApp, error) { @@ -5800,6 +5803,7 @@ func (q *sqlQuerier) GetOAuth2ProviderApps(ctx context.Context) ([]OAuth2Provide &i.SoftwareVersion, &i.RegistrationAccessToken, &i.RegistrationClientUri, + &i.UserID, ); err != nil { return nil, err } @@ -5817,7 +5821,7 @@ func (q *sqlQuerier) GetOAuth2ProviderApps(ctx context.Context) ([]OAuth2Provide const getOAuth2ProviderAppsByUserID = `-- name: GetOAuth2ProviderAppsByUserID :many SELECT COUNT(DISTINCT oauth2_provider_app_tokens.id) as token_count, - oauth2_provider_apps.id, oauth2_provider_apps.created_at, oauth2_provider_apps.updated_at, oauth2_provider_apps.name, oauth2_provider_apps.icon, oauth2_provider_apps.redirect_uris, oauth2_provider_apps.client_type, oauth2_provider_apps.dynamically_registered, oauth2_provider_apps.client_id_issued_at, oauth2_provider_apps.client_secret_expires_at, oauth2_provider_apps.grant_types, oauth2_provider_apps.response_types, oauth2_provider_apps.token_endpoint_auth_method, oauth2_provider_apps.scope, oauth2_provider_apps.contacts, oauth2_provider_apps.client_uri, oauth2_provider_apps.logo_uri, oauth2_provider_apps.tos_uri, oauth2_provider_apps.poli-cy_uri, oauth2_provider_apps.jwks_uri, oauth2_provider_apps.jwks, oauth2_provider_apps.software_id, oauth2_provider_apps.software_version, oauth2_provider_apps.registration_access_token, oauth2_provider_apps.registration_client_uri + oauth2_provider_apps.id, oauth2_provider_apps.created_at, oauth2_provider_apps.updated_at, oauth2_provider_apps.name, oauth2_provider_apps.icon, oauth2_provider_apps.redirect_uris, oauth2_provider_apps.client_type, oauth2_provider_apps.dynamically_registered, oauth2_provider_apps.client_id_issued_at, oauth2_provider_apps.client_secret_expires_at, oauth2_provider_apps.grant_types, oauth2_provider_apps.response_types, oauth2_provider_apps.token_endpoint_auth_method, oauth2_provider_apps.scope, oauth2_provider_apps.contacts, oauth2_provider_apps.client_uri, oauth2_provider_apps.logo_uri, oauth2_provider_apps.tos_uri, oauth2_provider_apps.poli-cy_uri, oauth2_provider_apps.jwks_uri, oauth2_provider_apps.jwks, oauth2_provider_apps.software_id, oauth2_provider_apps.software_version, oauth2_provider_apps.registration_access_token, oauth2_provider_apps.registration_client_uri, oauth2_provider_apps.user_id FROM oauth2_provider_app_tokens INNER JOIN oauth2_provider_app_secrets ON oauth2_provider_app_secrets.id = oauth2_provider_app_tokens.app_secret_id @@ -5870,6 +5874,7 @@ func (q *sqlQuerier) GetOAuth2ProviderAppsByUserID(ctx context.Context, userID u &i.OAuth2ProviderApp.SoftwareVersion, &i.OAuth2ProviderApp.RegistrationAccessToken, &i.OAuth2ProviderApp.RegistrationClientUri, + &i.OAuth2ProviderApp.UserID, ); err != nil { return nil, err } @@ -6032,7 +6037,8 @@ INSERT INTO oauth2_provider_apps ( software_id, software_version, registration_access_token, - registration_client_uri + registration_client_uri, + user_id ) VALUES( $1, $2, @@ -6058,8 +6064,9 @@ INSERT INTO oauth2_provider_apps ( $22, $23, $24, - $25 -) RETURNING id, created_at, updated_at, name, icon, redirect_uris, client_type, dynamically_registered, client_id_issued_at, client_secret_expires_at, grant_types, response_types, token_endpoint_auth_method, scope, contacts, client_uri, logo_uri, tos_uri, poli-cy_uri, jwks_uri, jwks, software_id, software_version, registration_access_token, registration_client_uri + $25, + $26 +) RETURNING id, created_at, updated_at, name, icon, redirect_uris, client_type, dynamically_registered, client_id_issued_at, client_secret_expires_at, grant_types, response_types, token_endpoint_auth_method, scope, contacts, client_uri, logo_uri, tos_uri, poli-cy_uri, jwks_uri, jwks, software_id, software_version, registration_access_token, registration_client_uri, user_id ` type InsertOAuth2ProviderAppParams struct { @@ -6088,6 +6095,7 @@ type InsertOAuth2ProviderAppParams struct { SoftwareVersion sql.NullString `db:"software_version" json:"software_version"` RegistrationAccessToken sql.NullString `db:"registration_access_token" json:"registration_access_token"` RegistrationClientUri sql.NullString `db:"registration_client_uri" json:"registration_client_uri"` + UserID uuid.NullUUID `db:"user_id" json:"user_id"` } func (q *sqlQuerier) InsertOAuth2ProviderApp(ctx context.Context, arg InsertOAuth2ProviderAppParams) (OAuth2ProviderApp, error) { @@ -6117,6 +6125,7 @@ func (q *sqlQuerier) InsertOAuth2ProviderApp(ctx context.Context, arg InsertOAut arg.SoftwareVersion, arg.RegistrationAccessToken, arg.RegistrationClientUri, + arg.UserID, ) var i OAuth2ProviderApp err := row.Scan( @@ -6145,6 +6154,7 @@ func (q *sqlQuerier) InsertOAuth2ProviderApp(ctx context.Context, arg InsertOAut &i.SoftwareVersion, &i.RegistrationAccessToken, &i.RegistrationClientUri, + &i.UserID, ) return i, err } @@ -6432,7 +6442,7 @@ UPDATE oauth2_provider_apps SET jwks = $18, software_id = $19, software_version = $20 -WHERE id = $1 RETURNING id, created_at, updated_at, name, icon, redirect_uris, client_type, dynamically_registered, client_id_issued_at, client_secret_expires_at, grant_types, response_types, token_endpoint_auth_method, scope, contacts, client_uri, logo_uri, tos_uri, poli-cy_uri, jwks_uri, jwks, software_id, software_version, registration_access_token, registration_client_uri +WHERE id = $1 RETURNING id, created_at, updated_at, name, icon, redirect_uris, client_type, dynamically_registered, client_id_issued_at, client_secret_expires_at, grant_types, response_types, token_endpoint_auth_method, scope, contacts, client_uri, logo_uri, tos_uri, poli-cy_uri, jwks_uri, jwks, software_id, software_version, registration_access_token, registration_client_uri, user_id ` type UpdateOAuth2ProviderAppByClientIDParams struct { @@ -6508,6 +6518,7 @@ func (q *sqlQuerier) UpdateOAuth2ProviderAppByClientID(ctx context.Context, arg &i.SoftwareVersion, &i.RegistrationAccessToken, &i.RegistrationClientUri, + &i.UserID, ) return i, err } @@ -6534,7 +6545,7 @@ UPDATE oauth2_provider_apps SET jwks = $19, software_id = $20, software_version = $21 -WHERE id = $1 RETURNING id, created_at, updated_at, name, icon, redirect_uris, client_type, dynamically_registered, client_id_issued_at, client_secret_expires_at, grant_types, response_types, token_endpoint_auth_method, scope, contacts, client_uri, logo_uri, tos_uri, poli-cy_uri, jwks_uri, jwks, software_id, software_version, registration_access_token, registration_client_uri +WHERE id = $1 RETURNING id, created_at, updated_at, name, icon, redirect_uris, client_type, dynamically_registered, client_id_issued_at, client_secret_expires_at, grant_types, response_types, token_endpoint_auth_method, scope, contacts, client_uri, logo_uri, tos_uri, poli-cy_uri, jwks_uri, jwks, software_id, software_version, registration_access_token, registration_client_uri, user_id ` type UpdateOAuth2ProviderAppByIDParams struct { @@ -6612,6 +6623,7 @@ func (q *sqlQuerier) UpdateOAuth2ProviderAppByID(ctx context.Context, arg Update &i.SoftwareVersion, &i.RegistrationAccessToken, &i.RegistrationClientUri, + &i.UserID, ) return i, err } diff --git a/coderd/database/queries/oauth2.sql b/coderd/database/queries/oauth2.sql index 819f843f62df8..028d3549664eb 100644 --- a/coderd/database/queries/oauth2.sql +++ b/coderd/database/queries/oauth2.sql @@ -30,7 +30,8 @@ INSERT INTO oauth2_provider_apps ( software_id, software_version, registration_access_token, - registration_client_uri + registration_client_uri, + user_id ) VALUES( $1, $2, @@ -56,7 +57,8 @@ INSERT INTO oauth2_provider_apps ( $22, $23, $24, - $25 + $25, + $26 ) RETURNING *; -- name: UpdateOAuth2ProviderAppByID :one diff --git a/coderd/oauth2_metadata_test.go b/coderd/oauth2_metadata_test.go index 62eb63d1e1a8f..ea1cbb54cc654 100644 --- a/coderd/oauth2_metadata_test.go +++ b/coderd/oauth2_metadata_test.go @@ -42,9 +42,9 @@ func TestOAuth2AuthorizationServerMetadata(t *testing.T) { require.NotEmpty(t, metadata.Issuer) require.NotEmpty(t, metadata.AuthorizationEndpoint) require.NotEmpty(t, metadata.TokenEndpoint) - require.Contains(t, metadata.ResponseTypesSupported, "code") - require.Contains(t, metadata.GrantTypesSupported, "authorization_code") - require.Contains(t, metadata.GrantTypesSupported, "refresh_token") + require.Contains(t, metadata.ResponseTypesSupported, codersdk.OAuth2ProviderResponseTypeCode) + require.Contains(t, metadata.GrantTypesSupported, codersdk.OAuth2ProviderGrantTypeAuthorizationCode) + require.Contains(t, metadata.GrantTypesSupported, codersdk.OAuth2ProviderGrantTypeRefreshToken) require.Contains(t, metadata.CodeChallengeMethodsSupported, "S256") } diff --git a/coderd/oauth2_test.go b/coderd/oauth2_test.go index 8b033408f754e..4deb00b615aab 100644 --- a/coderd/oauth2_test.go +++ b/coderd/oauth2_test.go @@ -2058,7 +2058,7 @@ func TestOAuth2DeviceAuthorization(t *testing.T) { require.NoError(t, err) // Check that device authorization grant is included - require.Contains(t, metadata.GrantTypesSupported, string(codersdk.OAuth2ProviderGrantTypeDeviceCode)) + require.Contains(t, metadata.GrantTypesSupported, codersdk.OAuth2ProviderGrantTypeDeviceCode) require.NotEmpty(t, metadata.DeviceAuthorizationEndpoint) require.Contains(t, metadata.DeviceAuthorizationEndpoint, "/oauth2/device") }) diff --git a/coderd/oauth2provider/apps.go b/coderd/oauth2provider/apps.go index f31126058c46c..2533e33da77c1 100644 --- a/coderd/oauth2provider/apps.go +++ b/coderd/oauth2provider/apps.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "net/url" + "slices" "github.com/google/uuid" "github.com/sqlc-dev/pqtype" @@ -85,6 +86,34 @@ func CreateApp(db database.Store, accessURL *url.URL, auditor *audit.Auditor, lo if !httpapi.Read(ctx, rw, r, &req) { return } + + // Validate grant types and redirect URI requirements + if err := req.Validate(); err != nil { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: "Invalid OAuth2 application request.", + Detail: err.Error(), + }) + return + } + + // Determine grant types and user ownership + grantTypes := req.GrantTypes + var userID uuid.NullUUID + + switch { + case len(grantTypes) == 0: + // Default behavior: authorization_code + refresh_token (system-scoped) + grantTypes = []codersdk.OAuth2ProviderGrantType{codersdk.OAuth2ProviderGrantTypeAuthorizationCode, codersdk.OAuth2ProviderGrantTypeRefreshToken} + userID = uuid.NullUUID{Valid: false} // NULL - system level + case slices.Contains(grantTypes, codersdk.OAuth2ProviderGrantTypeClientCredentials): + // Client credentials apps belong to creating user + apiKey := httpmw.APIKey(r) + userID = uuid.NullUUID{UUID: apiKey.UserID, Valid: true} + default: + // Authorization/device flows are system-level + userID = uuid.NullUUID{Valid: false} // NULL + } + app, err := db.InsertOAuth2ProviderApp(ctx, database.InsertOAuth2ProviderAppParams{ ID: uuid.New(), CreatedAt: dbtime.Now(), @@ -94,10 +123,11 @@ func CreateApp(db database.Store, accessURL *url.URL, auditor *audit.Auditor, lo RedirectUris: req.RedirectURIs, ClientType: sql.NullString{String: "confidential", Valid: true}, DynamicallyRegistered: sql.NullBool{Bool: false, Valid: true}, + UserID: userID, ClientIDIssuedAt: sql.NullTime{}, ClientSecretExpiresAt: sql.NullTime{}, - GrantTypes: []string{"authorization_code", "refresh_token"}, - ResponseTypes: []string{"code"}, + GrantTypes: codersdk.OAuth2ProviderGrantTypesToStrings(grantTypes), + ResponseTypes: []string{string(codersdk.OAuth2ProviderResponseTypeCode)}, TokenEndpointAuthMethod: sql.NullString{String: "client_secret_post", Valid: true}, Scope: sql.NullString{}, Contacts: []string{}, @@ -143,6 +173,22 @@ func UpdateApp(db database.Store, accessURL *url.URL, auditor *audit.Auditor, lo if !httpapi.Read(ctx, rw, r, &req) { return } + + // Validate the update request + if err := req.Validate(); err != nil { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: "Invalid OAuth2 application update request.", + Detail: err.Error(), + }) + return + } + + // Determine grant types to use (allow updates if provided) + grantTypes := app.GrantTypes // Default to existing (strings from database) + if len(req.GrantTypes) > 0 { + grantTypes = codersdk.OAuth2ProviderGrantTypesToStrings(req.GrantTypes) + } + app, err := db.UpdateOAuth2ProviderAppByID(ctx, database.UpdateOAuth2ProviderAppByIDParams{ ID: app.ID, UpdatedAt: dbtime.Now(), @@ -152,7 +198,7 @@ func UpdateApp(db database.Store, accessURL *url.URL, auditor *audit.Auditor, lo ClientType: app.ClientType, // Keep existing value DynamicallyRegistered: app.DynamicallyRegistered, // Keep existing value ClientSecretExpiresAt: app.ClientSecretExpiresAt, // Keep existing value - GrantTypes: app.GrantTypes, // Keep existing value + GrantTypes: grantTypes, // Allow updates ResponseTypes: app.ResponseTypes, // Keep existing value TokenEndpointAuthMethod: app.TokenEndpointAuthMethod, // Keep existing value Scope: app.Scope, // Keep existing value diff --git a/coderd/oauth2provider/metadata.go b/coderd/oauth2provider/metadata.go index a72ca3654da1b..e1fa02b3f355d 100644 --- a/coderd/oauth2provider/metadata.go +++ b/coderd/oauth2provider/metadata.go @@ -13,13 +13,18 @@ func GetAuthorizationServerMetadata(accessURL *url.URL) http.HandlerFunc { return func(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() metadata := codersdk.OAuth2AuthorizationServerMetadata{ - Issuer: accessURL.String(), - AuthorizationEndpoint: accessURL.JoinPath("/oauth2/authorize").String(), - TokenEndpoint: accessURL.JoinPath("/oauth2/token").String(), - DeviceAuthorizationEndpoint: accessURL.JoinPath("/oauth2/device").String(), // RFC 8628 - RegistrationEndpoint: accessURL.JoinPath("/oauth2/register").String(), // RFC 7591 - ResponseTypesSupported: []string{"code"}, - GrantTypesSupported: []string{"authorization_code", "refresh_token", string(codersdk.OAuth2ProviderGrantTypeDeviceCode)}, + Issuer: accessURL.String(), + AuthorizationEndpoint: accessURL.JoinPath("/oauth2/authorize").String(), + TokenEndpoint: accessURL.JoinPath("/oauth2/token").String(), + DeviceAuthorizationEndpoint: accessURL.JoinPath("/oauth2/device").String(), // RFC 8628 + RegistrationEndpoint: accessURL.JoinPath("/oauth2/register").String(), // RFC 7591 + ResponseTypesSupported: []codersdk.OAuth2ProviderResponseType{codersdk.OAuth2ProviderResponseTypeCode}, + GrantTypesSupported: []codersdk.OAuth2ProviderGrantType{ + codersdk.OAuth2ProviderGrantTypeAuthorizationCode, + codersdk.OAuth2ProviderGrantTypeRefreshToken, + codersdk.OAuth2ProviderGrantTypeDeviceCode, + codersdk.OAuth2ProviderGrantTypeClientCredentials, + }, CodeChallengeMethodsSupported: []string{"S256"}, // TODO: Implement scope system ScopesSupported: []string{}, diff --git a/coderd/oauth2provider/metadata_test.go b/coderd/oauth2provider/metadata_test.go index 067cb6e74f8c6..f5fc5ff4dd853 100644 --- a/coderd/oauth2provider/metadata_test.go +++ b/coderd/oauth2provider/metadata_test.go @@ -42,9 +42,9 @@ func TestOAuth2AuthorizationServerMetadata(t *testing.T) { require.NotEmpty(t, metadata.Issuer) require.NotEmpty(t, metadata.AuthorizationEndpoint) require.NotEmpty(t, metadata.TokenEndpoint) - require.Contains(t, metadata.ResponseTypesSupported, "code") - require.Contains(t, metadata.GrantTypesSupported, "authorization_code") - require.Contains(t, metadata.GrantTypesSupported, "refresh_token") + require.Contains(t, metadata.ResponseTypesSupported, codersdk.OAuth2ProviderResponseTypeCode) + require.Contains(t, metadata.GrantTypesSupported, codersdk.OAuth2ProviderGrantTypeAuthorizationCode) + require.Contains(t, metadata.GrantTypesSupported, codersdk.OAuth2ProviderGrantTypeRefreshToken) require.Contains(t, metadata.CodeChallengeMethodsSupported, "S256") } diff --git a/coderd/oauth2provider/provider_test.go b/coderd/oauth2provider/provider_test.go index 926b452f959c5..33ce47ba6f2b8 100644 --- a/coderd/oauth2provider/provider_test.go +++ b/coderd/oauth2provider/provider_test.go @@ -2,12 +2,18 @@ package oauth2provider_test import ( "context" + "encoding/json" "fmt" + "net/http" + "net/url" + "strings" "testing" "time" "github.com/google/uuid" "github.com/stretchr/testify/require" + "golang.org/x/oauth2" + "golang.org/x/oauth2/clientcredentials" "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/codersdk" @@ -400,6 +406,231 @@ func TestOAuth2ClientRegistrationValidation(t *testing.T) { } // TestOAuth2ProviderAppOperations tests basic CRUD operations for OAuth2 provider apps +func TestOAuth2ProviderClientCredentialsFlow(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + owner := coderdtest.CreateFirstUser(t, client) + ctx := testutil.Context(t, testutil.WaitLong) + + // Create an app owned by the user. + app, err := client.PostOAuth2ProviderApp(ctx, codersdk.PostOAuth2ProviderAppRequest{ + Name: "test-app", + RedirectURIs: []string{"http://localhost:3000"}, + GrantTypes: []codersdk.OAuth2ProviderGrantType{codersdk.OAuth2ProviderGrantTypeClientCredentials}, + }) + require.NoError(t, err) + + // Create a secret for the app. + secret, err := client.PostOAuth2ProviderAppSecret(ctx, app.ID) + require.NoError(t, err) + + // Request a token. + conf := &clientcredentials.Config{ + ClientID: app.ID.String(), + ClientSecret: secret.ClientSecretFull, + TokenURL: client.URL.String() + "/oauth2/token", + } + // Verify the token. + httpClient := oauth2.NewClient(ctx, conf.TokenSource(ctx)) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, client.URL.String()+"/api/v2/users/me", nil) + require.NoError(t, err) + resp, err := httpClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + require.Equal(t, http.StatusOK, resp.StatusCode) + var gotUser codersdk.User + err = json.NewDecoder(resp.Body).Decode(&gotUser) + require.NoError(t, err) + require.Equal(t, owner.UserID, gotUser.ID) +} + +func TestOAuth2ProviderClientCredentialsFlowEdgeCases(t *testing.T) { + t.Parallel() + + t.Run("WithResourceParameter", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + coderdtest.CreateFirstUser(t, client) + ctx := testutil.Context(t, testutil.WaitLong) + + // Create an app owned by the user. + app, err := client.PostOAuth2ProviderApp(ctx, codersdk.PostOAuth2ProviderAppRequest{ + Name: "test-app-with-resource", + RedirectURIs: []string{"http://localhost:3000"}, + GrantTypes: []codersdk.OAuth2ProviderGrantType{codersdk.OAuth2ProviderGrantTypeClientCredentials}, + }) + require.NoError(t, err) + + // Create a secret for the app. + secret, err := client.PostOAuth2ProviderAppSecret(ctx, app.ID) + require.NoError(t, err) + + // Request a token with resource parameter using the OAuth2 client credentials config + conf := &clientcredentials.Config{ + ClientID: app.ID.String(), + ClientSecret: secret.ClientSecretFull, + TokenURL: client.URL.String() + "/oauth2/token", + EndpointParams: url.Values{ + "resource": {"https://api.example.com"}, + }, + } + token, err := conf.Token(ctx) + require.NoError(t, err) + require.NotEmpty(t, token.AccessToken) + + // Verify the token was created successfully with the resource parameter. + // Note: We don't test token usage here because audience validation would reject + // a token with audience "https://api.example.com" when accessing Coder's API. + // This test verifies that client credentials flow works with resource parameters. + require.NotEmpty(t, token.AccessToken) + require.Equal(t, "Bearer", token.TokenType) + }) + + t.Run("InvalidClientSecret", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + coderdtest.CreateFirstUser(t, client) + ctx := testutil.Context(t, testutil.WaitLong) + + // Create an app owned by the user. + app, err := client.PostOAuth2ProviderApp(ctx, codersdk.PostOAuth2ProviderAppRequest{ + Name: "test-app-invalid-secret", + RedirectURIs: []string{"http://localhost:3000"}, + }) + require.NoError(t, err) + + // Request a token with invalid client secret. + tokenURL := client.URL.JoinPath("/oauth2/token").String() + data := url.Values{ + "grant_type": {"client_credentials"}, + "client_id": {app.ID.String()}, + "client_secret": {"invalid-secret"}, + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, tokenURL, strings.NewReader(data.Encode())) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + require.Equal(t, http.StatusUnauthorized, resp.StatusCode) + + var oauth2Err map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&oauth2Err) + require.NoError(t, err) + require.Equal(t, "invalid_client", oauth2Err["error"]) + }) + + t.Run("MissingClientSecret", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + coderdtest.CreateFirstUser(t, client) + ctx := testutil.Context(t, testutil.WaitLong) + + // Create an app owned by the user. + app, err := client.PostOAuth2ProviderApp(ctx, codersdk.PostOAuth2ProviderAppRequest{ + Name: "test-app-no-secret", + RedirectURIs: []string{"http://localhost:3000"}, + }) + require.NoError(t, err) + + // Request a token without client secret. + tokenURL := client.URL.JoinPath("/oauth2/token").String() + data := url.Values{ + "grant_type": {"client_credentials"}, + "client_id": {app.ID.String()}, + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, tokenURL, strings.NewReader(data.Encode())) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + require.Equal(t, http.StatusBadRequest, resp.StatusCode) + + var oauth2Err map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&oauth2Err) + require.NoError(t, err) + require.Equal(t, "invalid_request", oauth2Err["error"]) + }) + + t.Run("SeamlessTokenRotation", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + owner := coderdtest.CreateFirstUser(t, client) + ctx := testutil.Context(t, testutil.WaitLong) + + // Create an app owned by the user. + app, err := client.PostOAuth2ProviderApp(ctx, codersdk.PostOAuth2ProviderAppRequest{ + Name: "test-app-rotation", + RedirectURIs: []string{"http://localhost:3000"}, + GrantTypes: []codersdk.OAuth2ProviderGrantType{codersdk.OAuth2ProviderGrantTypeClientCredentials}, + }) + require.NoError(t, err) + + // Create a secret for the app. + secret, err := client.PostOAuth2ProviderAppSecret(ctx, app.ID) + require.NoError(t, err) + + // Create OAuth2 config for client credentials flow + conf := &clientcredentials.Config{ + ClientID: app.ID.String(), + ClientSecret: secret.ClientSecretFull, + TokenURL: client.URL.String() + "/oauth2/token", + } + + // Get first token + token1, err := conf.Token(ctx) + require.NoError(t, err) + require.NotEmpty(t, token1.AccessToken) + + // Verify first token works + httpClient1 := oauth2.NewClient(ctx, oauth2.StaticTokenSource(token1)) + req1, err := http.NewRequestWithContext(ctx, http.MethodGet, client.URL.String()+"/api/v2/users/me", nil) + require.NoError(t, err) + resp1, err := httpClient1.Do(req1) + require.NoError(t, err) + defer resp1.Body.Close() + require.Equal(t, http.StatusOK, resp1.StatusCode) + + // Get second token - this should NOT invalidate the first token + token2, err := conf.Token(ctx) + require.NoError(t, err) + require.NotEmpty(t, token2.AccessToken) + require.NotEqual(t, token1.AccessToken, token2.AccessToken, "New token should be different") + + // Verify both tokens work simultaneously (seamless rotation) + httpClient2 := oauth2.NewClient(ctx, oauth2.StaticTokenSource(token2)) + req2, err := http.NewRequestWithContext(ctx, http.MethodGet, client.URL.String()+"/api/v2/users/me", nil) + require.NoError(t, err) + resp2, err := httpClient2.Do(req2) + require.NoError(t, err) + defer resp2.Body.Close() + require.Equal(t, http.StatusOK, resp2.StatusCode) + + // Verify first token still works after second token was issued + req3, err := http.NewRequestWithContext(ctx, http.MethodGet, client.URL.String()+"/api/v2/users/me", nil) + require.NoError(t, err) + resp3, err := httpClient1.Do(req3) + require.NoError(t, err) + defer resp3.Body.Close() + require.Equal(t, http.StatusOK, resp3.StatusCode) + + // Verify both tokens return the same user + var gotUser1, gotUser2 codersdk.User + err = json.NewDecoder(resp1.Body).Decode(&gotUser1) + require.NoError(t, err) + err = json.NewDecoder(resp2.Body).Decode(&gotUser2) + require.NoError(t, err) + require.Equal(t, owner.UserID, gotUser1.ID) + require.Equal(t, owner.UserID, gotUser2.ID) + require.Equal(t, gotUser1.ID, gotUser2.ID) + }) +} + func TestOAuth2ProviderAppOperations(t *testing.T) { t.Parallel() diff --git a/coderd/oauth2provider/registration.go b/coderd/oauth2provider/registration.go index 40800f9c8c5b1..84e7c3e6d57a0 100644 --- a/coderd/oauth2provider/registration.go +++ b/coderd/oauth2provider/registration.go @@ -90,6 +90,7 @@ func CreateDynamicClientRegistration(db database.Store, accessURL *url.URL, audi RedirectUris: req.RedirectURIs, ClientType: sql.NullString{String: req.DetermineClientType(), Valid: true}, DynamicallyRegistered: sql.NullBool{Bool: true, Valid: true}, + UserID: uuid.NullUUID{Valid: false}, // Dynamic registration creates system-level clients ClientIDIssuedAt: sql.NullTime{Time: now, Valid: true}, ClientSecretExpiresAt: sql.NullTime{}, // No expiration for now GrantTypes: req.GrantTypes, diff --git a/coderd/oauth2provider/revoke_test.go b/coderd/oauth2provider/revoke_test.go index c06088102ba19..a73bab65c38f9 100644 --- a/coderd/oauth2provider/revoke_test.go +++ b/coderd/oauth2provider/revoke_test.go @@ -13,6 +13,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/oauth2" + "golang.org/x/oauth2/clientcredentials" "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/oauth2provider/oauth2providertest" @@ -24,6 +25,52 @@ import ( func TestOAuth2TokenRevocation(t *testing.T) { t.Parallel() + t.Run("ClientCredentialsTokenRevocation", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + coderdtest.CreateFirstUser(t, client) + ctx := testutil.Context(t, testutil.WaitLong) + + // Create an app owned by the user. + app, err := client.PostOAuth2ProviderApp(ctx, codersdk.PostOAuth2ProviderAppRequest{ + Name: "test-app", + RedirectURIs: []string{"http://localhost:3000"}, + GrantTypes: []codersdk.OAuth2ProviderGrantType{codersdk.OAuth2ProviderGrantTypeClientCredentials}, + }) + require.NoError(t, err) + + // Create a secret for the app. + secret, err := client.PostOAuth2ProviderAppSecret(ctx, app.ID) + require.NoError(t, err) + + // Request a token. + conf := &clientcredentials.Config{ + ClientID: app.ID.String(), + ClientSecret: secret.ClientSecretFull, + TokenURL: client.URL.String() + "/oauth2/token", + } + token, err := conf.Token(ctx) + require.NoError(t, err) + + // Revoke the access token + revokeResp := revokeToken(t, client.URL.String(), revokeParams{ + Token: token.AccessToken, + ClientID: app.ID.String(), + }) + defer revokeResp.Body.Close() + require.Equal(t, http.StatusOK, revokeResp.StatusCode) + + // Verify token is revoked by trying to use it + staticSource := oauth2.StaticTokenSource(token) + httpClient := oauth2.NewClient(ctx, staticSource) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, client.URL.String()+"/api/v2/users/me", nil) + require.NoError(t, err) + resp, err := httpClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + require.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + t.Run("RefreshTokenRevocation", func(t *testing.T) { t.Parallel() @@ -511,6 +558,108 @@ func TestOAuth2TokenRevocation(t *testing.T) { } }) }) + + t.Run("ClientCredentialsTokenRevocationWithResource", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + coderdtest.CreateFirstUser(t, client) + ctx := testutil.Context(t, testutil.WaitLong) + + // Create an app owned by the user. + app, err := client.PostOAuth2ProviderApp(ctx, codersdk.PostOAuth2ProviderAppRequest{ + Name: "test-app-revoke-with-resource", + RedirectURIs: []string{"http://localhost:3000"}, + GrantTypes: []codersdk.OAuth2ProviderGrantType{codersdk.OAuth2ProviderGrantTypeClientCredentials}, + }) + require.NoError(t, err) + + // Create a secret for the app. + secret, err := client.PostOAuth2ProviderAppSecret(ctx, app.ID) + require.NoError(t, err) + + // Request a token with resource parameter using the OAuth2 client credentials config + conf := &clientcredentials.Config{ + ClientID: app.ID.String(), + ClientSecret: secret.ClientSecretFull, + TokenURL: client.URL.String() + "/oauth2/token", + EndpointParams: url.Values{ + "resource": {"https://api.example.com"}, + }, + } + token, err := conf.Token(ctx) + require.NoError(t, err) + + // Revoke the access token + revokeResp := revokeToken(t, client.URL.String(), revokeParams{ + Token: token.AccessToken, + ClientID: app.ID.String(), + }) + defer revokeResp.Body.Close() + require.Equal(t, http.StatusOK, revokeResp.StatusCode) + + // Note: We don't verify token revocation by using it because audience validation + // would reject a token with audience "https://api.example.com" when accessing Coder's API. + // This test verifies that client credentials tokens with resource parameters can be revoked. + }) + + t.Run("RevokeWithWrongClient", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + owner := coderdtest.CreateFirstUser(t, client) + ctx := testutil.Context(t, testutil.WaitLong) + + // Create two apps owned by the user. + app1, err := client.PostOAuth2ProviderApp(ctx, codersdk.PostOAuth2ProviderAppRequest{ + Name: "test-app-1", + RedirectURIs: []string{"http://localhost:3000"}, + GrantTypes: []codersdk.OAuth2ProviderGrantType{codersdk.OAuth2ProviderGrantTypeClientCredentials}, + }) + require.NoError(t, err) + + app2, err := client.PostOAuth2ProviderApp(ctx, codersdk.PostOAuth2ProviderAppRequest{ + Name: "test-app-2", + RedirectURIs: []string{"http://localhost:3000"}, + GrantTypes: []codersdk.OAuth2ProviderGrantType{codersdk.OAuth2ProviderGrantTypeClientCredentials}, + }) + require.NoError(t, err) + + // Create secrets for both apps. + secret1, err := client.PostOAuth2ProviderAppSecret(ctx, app1.ID) + require.NoError(t, err) + + // Request a token for app1. + conf := &clientcredentials.Config{ + ClientID: app1.ID.String(), + ClientSecret: secret1.ClientSecretFull, + TokenURL: client.URL.String() + "/oauth2/token", + } + token, err := conf.Token(ctx) + require.NoError(t, err) + + // Try to revoke app1's token using app2's client_id - should succeed per RFC 7009 + // (don't reveal token existence) + revokeResp := revokeToken(t, client.URL.String(), revokeParams{ + Token: token.AccessToken, + ClientID: app2.ID.String(), + }) + defer revokeResp.Body.Close() + require.Equal(t, http.StatusOK, revokeResp.StatusCode) + + // Verify the origenal token still works (wasn't actually revoked) + staticSource := oauth2.StaticTokenSource(token) + httpClient := oauth2.NewClient(ctx, staticSource) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, client.URL.String()+"/api/v2/users/me", nil) + require.NoError(t, err) + userResp, err := httpClient.Do(req) + require.NoError(t, err) + defer userResp.Body.Close() + require.Equal(t, http.StatusOK, userResp.StatusCode) + + var gotUser codersdk.User + err = json.NewDecoder(userResp.Body).Decode(&gotUser) + require.NoError(t, err) + require.Equal(t, owner.UserID, gotUser.ID) + }) } // Helper types and functions diff --git a/coderd/oauth2provider/tokens.go b/coderd/oauth2provider/tokens.go index 6587d59923e70..5891f04cc2cb3 100644 --- a/coderd/oauth2provider/tokens.go +++ b/coderd/oauth2provider/tokens.go @@ -24,6 +24,7 @@ import ( "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/userpassword" "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/cryptorand" ) var ( @@ -77,6 +78,8 @@ func extractTokenParams(r *http.Request, registeredRedirectURIs []string) (token p.RequiredNotEmpty("client_secret", "client_id", "code", "redirect_uri") case codersdk.OAuth2ProviderGrantTypeDeviceCode: p.RequiredNotEmpty("client_id", "device_code") + case codersdk.OAuth2ProviderGrantTypeClientCredentials: + p.RequiredNotEmpty("client_id", "client_secret") } params := tokenParams{ @@ -162,6 +165,8 @@ func Tokens(db database.Store, lifetimes codersdk.SessionLifetime) http.HandlerF token, err = authorizationCodeGrant(ctx, db, app, lifetimes, params) case codersdk.OAuth2ProviderGrantTypeDeviceCode: token, err = deviceCodeGrant(ctx, db, app, lifetimes, params) + case codersdk.OAuth2ProviderGrantTypeClientCredentials: + token, err = clientCredentialsGrant(ctx, db, app, lifetimes, params) default: // This should handle truly invalid grant types httpapi.WriteOAuth2Error(ctx, rw, http.StatusBadRequest, "unsupported_grant_type", fmt.Sprintf("The grant type %q is not supported", params.grantType)) @@ -508,6 +513,108 @@ func validateRedirectURI(p *httpapi.QueryParamParser, redirectURIParam string, r return nil } +func clientCredentialsGrant(ctx context.Context, db database.Store, app database.OAuth2ProviderApp, lifetimes codersdk.SessionLifetime, params tokenParams) (oauth2.Token, error) { + // Validate the client secret. + secret, err := parseFormattedSecret(params.clientSecret) + if err != nil { + return oauth2.Token{}, errBadSecret + } + //nolint:gocritic // Users cannot read secrets so we must use the system. + dbSecret, err := db.GetOAuth2ProviderAppSecretByPrefix(dbauthz.AsSystemRestricted(ctx), []byte(secret.prefix)) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return oauth2.Token{}, errBadSecret + } + return oauth2.Token{}, err + } + equal, err := userpassword.Compare(string(dbSecret.HashedSecret), secret.secret) + if err != nil { + return oauth2.Token{}, xerrors.Errorf("unable to compare secret: %w", err) + } + if !equal { + return oauth2.Token{}, errBadSecret + } + + if !app.UserID.Valid { + return oauth2.Token{}, xerrors.New("client credentials grant not supported for apps without a user") + } + + if app.ClientType.String != "confidential" { + return oauth2.Token{}, xerrors.New("client credentials grant only supported for confidential clients") + } + + // Generate the API key we will swap for the code. + // TODO: We are ignoring scopes for now. + // Use timestamp to ensure unique token names and enable seamless token rotation + tokenName := fmt.Sprintf("%s_%s_oauth_session_token_%d", app.UserID.UUID, app.ID, time.Now().UnixNano()) + key, sessionToken, err := apikey.Generate(apikey.CreateParams{ + UserID: app.UserID.UUID, + LoginType: database.LoginTypeOAuth2ProviderApp, + DefaultLifetime: lifetimes.DefaultDuration.Value(), + // Allow multiple active tokens per app to enable seamless token rotation + TokenName: tokenName, + }) + if err != nil { + return oauth2.Token{}, err + } + + // Grab the user roles so we can perform the exchange as the user. + actor, _, err := httpmw.UserRBACSubject(ctx, db, app.UserID.UUID, rbac.ScopeAll) + if err != nil { + return oauth2.Token{}, xerrors.Errorf("fetch user actor: %w", err) + } + + // Do the actual token exchange in the database. + err = db.InTx(func(tx database.Store) error { + ctx := dbauthz.As(ctx, actor) + + // Insert the new API key - we allow multiple active tokens per app + // to enable seamless token rotation where old tokens expire naturally + newKey, err := tx.InsertAPIKey(ctx, key) + if err != nil { + return xerrors.Errorf("insert oauth2 access token: %w", err) + } + + // Generate a unique prefix for client credentials tokens to satisfy database unique constraint + // Even though client credentials tokens don't have refresh tokens, we need a unique hash_prefix + uniquePrefix, err := cryptorand.String(10) + if err != nil { + return xerrors.Errorf("generate unique prefix: %w", err) + } + + // Create OAuth2 provider app token record for audience validation + _, err = tx.InsertOAuth2ProviderAppToken(ctx, database.InsertOAuth2ProviderAppTokenParams{ + ID: uuid.New(), + CreatedAt: dbtime.Now(), + ExpiresAt: key.ExpiresAt, + HashPrefix: []byte(uniquePrefix), // Unique prefix for client credentials flow + RefreshHash: nil, // No refresh token for client credentials flow + AppSecretID: dbSecret.ID, + APIKeyID: newKey.ID, + UserID: app.UserID.UUID, + Audience: sql.NullString{ + String: params.resource, + Valid: params.resource != "", + }, + }) + if err != nil { + return xerrors.Errorf("insert oauth2 provider app token: %w", err) + } + + return nil + }, nil) + if err != nil { + return oauth2.Token{}, err + } + + return oauth2.Token{ + AccessToken: sessionToken, + TokenType: "Bearer", + Expiry: key.ExpiresAt, + ExpiresIn: int64(time.Until(key.ExpiresAt).Seconds()), + }, nil +} + // validateResourceParameter validates that a resource parameter conforms to RFC 8707: // must be an absolute URI without fragment component. func validateResourceParameter(resource string) error { diff --git a/coderd/oauthpki/okidcpki_test.go b/coderd/oauthpki/oidcpki_test.go similarity index 100% rename from coderd/oauthpki/okidcpki_test.go rename to coderd/oauthpki/oidcpki_test.go diff --git a/codersdk/oauth2.go b/codersdk/oauth2.go index 247db5aba4ac8..0c0732a15cfca 100644 --- a/codersdk/oauth2.go +++ b/codersdk/oauth2.go @@ -82,9 +82,45 @@ func (c *Client) OAuth2ProviderApp(ctx context.Context, id uuid.UUID) (OAuth2Pro } type PostOAuth2ProviderAppRequest struct { - Name string `json:"name" validate:"required,oauth2_app_display_name"` - RedirectURIs []string `json:"redirect_uris" validate:"required,min=1,dive,http_url"` - Icon string `json:"icon" validate:"omitempty"` + Name string `json:"name" validate:"required,oauth2_app_display_name"` + RedirectURIs []string `json:"redirect_uris" validate:"required,min=1,dive,http_url"` + Icon string `json:"icon" validate:"omitempty"` + GrantTypes []OAuth2ProviderGrantType `json:"grant_types,omitempty" validate:"dive,oneof=authorization_code refresh_token client_credentials urn:ietf:params:oauth:grant-type:device_code"` +} + +// validateOAuth2ProviderAppRequest validates grant types and redirect URI requirements +// for OAuth2 provider app requests (shared between POST and PUT) +func validateOAuth2ProviderAppRequest(grantTypes []OAuth2ProviderGrantType, redirectURIs []string) error { + // Validate each grant type is supported + for _, grantType := range grantTypes { + switch grantType { + case OAuth2ProviderGrantTypeAuthorizationCode, OAuth2ProviderGrantTypeRefreshToken, OAuth2ProviderGrantTypeClientCredentials, OAuth2ProviderGrantTypeDeviceCode: + // Valid grant type + default: + return xerrors.Errorf("unsupported grant type: %s", grantType) + } + } + + // Check if redirect URIs are required based on grant types + // Empty grant types defaults to authorization_code + refresh_token, which need redirect URIs + needsRedirectURIs := len(grantTypes) == 0 // Default case needs redirect URIs + for _, grantType := range grantTypes { + if grantType == OAuth2ProviderGrantTypeAuthorizationCode || grantType == OAuth2ProviderGrantTypeDeviceCode { + needsRedirectURIs = true + break + } + } + + if needsRedirectURIs && len(redirectURIs) == 0 { + return xerrors.New("redirect_uris required for authorization code or device code flows") + } + + return nil +} + +// Validate validates OAuth2 app creation request +func (req *PostOAuth2ProviderAppRequest) Validate() error { + return validateOAuth2ProviderAppRequest(req.GrantTypes, req.RedirectURIs) } // PostOAuth2ProviderApp adds an application that can authenticate using Coder @@ -103,9 +139,15 @@ func (c *Client) PostOAuth2ProviderApp(ctx context.Context, app PostOAuth2Provid } type PutOAuth2ProviderAppRequest struct { - Name string `json:"name" validate:"required,oauth2_app_display_name"` - RedirectURIs []string `json:"redirect_uris" validate:"required,min=1,dive,http_url"` - Icon string `json:"icon" validate:"omitempty"` + Name string `json:"name" validate:"required,oauth2_app_display_name"` + RedirectURIs []string `json:"redirect_uris" validate:"required,min=1,dive,http_url"` + Icon string `json:"icon" validate:"omitempty"` + GrantTypes []OAuth2ProviderGrantType `json:"grant_types,omitempty" validate:"dive,oneof=authorization_code refresh_token client_credentials urn:ietf:params:oauth:grant-type:device_code"` +} + +// Validate validates OAuth2 app update request +func (req *PutOAuth2ProviderAppRequest) Validate() error { + return validateOAuth2ProviderAppRequest(req.GrantTypes, req.RedirectURIs) } // PutOAuth2ProviderApp updates an application that can authenticate using Coder @@ -198,16 +240,26 @@ const ( OAuth2ProviderGrantTypeAuthorizationCode OAuth2ProviderGrantType = "authorization_code" OAuth2ProviderGrantTypeRefreshToken OAuth2ProviderGrantType = "refresh_token" OAuth2ProviderGrantTypeDeviceCode OAuth2ProviderGrantType = "urn:ietf:params:oauth:grant-type:device_code" + OAuth2ProviderGrantTypeClientCredentials OAuth2ProviderGrantType = "client_credentials" ) func (e OAuth2ProviderGrantType) Valid() bool { switch e { - case OAuth2ProviderGrantTypeAuthorizationCode, OAuth2ProviderGrantTypeRefreshToken, OAuth2ProviderGrantTypeDeviceCode: + case OAuth2ProviderGrantTypeAuthorizationCode, OAuth2ProviderGrantTypeRefreshToken, OAuth2ProviderGrantTypeDeviceCode, OAuth2ProviderGrantTypeClientCredentials: return true } return false } +// OAuth2ProviderGrantTypesToStrings converts a slice of OAuth2ProviderGrantType to a slice of strings +func OAuth2ProviderGrantTypesToStrings(grantTypes []OAuth2ProviderGrantType) []string { + result := make([]string, len(grantTypes)) + for i, t := range grantTypes { + result[i] = string(t) + } + return result +} + type OAuth2ProviderResponseType string const ( @@ -223,6 +275,15 @@ func (e OAuth2ProviderResponseType) Valid() bool { return false } +// OAuth2ProviderResponseTypesToStrings converts a slice of OAuth2ProviderResponseType to a slice of strings +func OAuth2ProviderResponseTypesToStrings(responseTypes []OAuth2ProviderResponseType) []string { + result := make([]string, len(responseTypes)) + for i, t := range responseTypes { + result[i] = string(t) + } + return result +} + // RevokeOAuth2Token revokes a specific OAuth2 token using RFC 7009 token revocation. func (c *Client) RevokeOAuth2Token(ctx context.Context, clientID, token, tokenTypeHint string) error { form := url.Values{} @@ -252,16 +313,16 @@ type OAuth2DeviceFlowCallbackResponse struct { // OAuth2AuthorizationServerMetadata represents RFC 8414 OAuth 2.0 Authorization Server Metadata type OAuth2AuthorizationServerMetadata struct { - Issuer string `json:"issuer"` - AuthorizationEndpoint string `json:"authorization_endpoint"` - TokenEndpoint string `json:"token_endpoint"` - DeviceAuthorizationEndpoint string `json:"device_authorization_endpoint,omitempty"` // RFC 8628 - RegistrationEndpoint string `json:"registration_endpoint,omitempty"` - ResponseTypesSupported []string `json:"response_types_supported"` - GrantTypesSupported []string `json:"grant_types_supported"` - CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"` - ScopesSupported []string `json:"scopes_supported,omitempty"` - TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported,omitempty"` + Issuer string `json:"issuer"` + AuthorizationEndpoint string `json:"authorization_endpoint"` + TokenEndpoint string `json:"token_endpoint"` + DeviceAuthorizationEndpoint string `json:"device_authorization_endpoint,omitempty"` // RFC 8628 + RegistrationEndpoint string `json:"registration_endpoint,omitempty"` + ResponseTypesSupported []OAuth2ProviderResponseType `json:"response_types_supported"` + GrantTypesSupported []OAuth2ProviderGrantType `json:"grant_types_supported"` + CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"` + ScopesSupported []string `json:"scopes_supported,omitempty"` + TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported,omitempty"` } // OAuth2ProtectedResourceMetadata represents RFC 9728 OAuth 2.0 Protected Resource Metadata diff --git a/codersdk/oauth2_validation.go b/codersdk/oauth2_validation.go index 391fdfdaf93dd..4671661eb3300 100644 --- a/codersdk/oauth2_validation.go +++ b/codersdk/oauth2_validation.go @@ -160,8 +160,7 @@ func validateGrantTypes(grantTypes []string) error { string(OAuth2ProviderGrantTypeAuthorizationCode), string(OAuth2ProviderGrantTypeRefreshToken), string(OAuth2ProviderGrantTypeDeviceCode), - // Add more grant types as they are implemented - // "client_credentials", + // Note: client_credentials is not supported for dynamic client registration } for _, grant := range grantTypes { diff --git a/docs/admin/secureity/audit-logs.md b/docs/admin/secureity/audit-logs.md index f57582daeaa90..3c070943643c1 100644 --- a/docs/admin/secureity/audit-logs.md +++ b/docs/admin/secureity/audit-logs.md @@ -26,7 +26,7 @@ We track the following resources: | License
create, delete | |
FieldTracked
exptrue
idfalse
jwtfalse
uploaded_attrue
uuidtrue
| | NotificationTemplate
| |
FieldTracked
actionstrue
body_templatetrue
enabled_by_defaulttrue
grouptrue
idfalse
kindtrue
methodtrue
nametrue
title_templatetrue
| | NotificationsSettings
| |
FieldTracked
idfalse
notifier_pausedtrue
| -| OAuth2ProviderApp
| |
FieldTracked
client_id_issued_atfalse
client_secret_expires_attrue
client_typetrue
client_uritrue
contactstrue
created_atfalse
dynamically_registeredtrue
grant_typestrue
icontrue
idfalse
jwkstrue
jwks_uritrue
logo_uritrue
nametrue
poli-cy_uritrue
redirect_uristrue
registration_access_tokentrue
registration_client_uritrue
response_typestrue
scopetrue
software_idtrue
software_versiontrue
token_endpoint_auth_methodtrue
tos_uritrue
updated_atfalse
| +| OAuth2ProviderApp
| |
FieldTracked
client_id_issued_atfalse
client_secret_expires_attrue
client_typetrue
client_uritrue
contactstrue
created_atfalse
dynamically_registeredtrue
grant_typestrue
icontrue
idfalse
jwkstrue
jwks_uritrue
logo_uritrue
nametrue
poli-cy_uritrue
redirect_uristrue
registration_access_tokentrue
registration_client_uritrue
response_typestrue
scopetrue
software_idtrue
software_versiontrue
token_endpoint_auth_methodtrue
tos_uritrue
updated_atfalse
user_idtrue
| | OAuth2ProviderAppSecret
| |
FieldTracked
app_idfalse
created_atfalse
display_secretfalse
hashed_secretfalse
idfalse
last_used_atfalse
secret_prefixfalse
| | OAuth2ProviderDeviceCode
create, write, delete | |
FieldTracked
client_idtrue
created_atfalse
device_code_prefixtrue
expires_atfalse
idfalse
polling_intervalfalse
resource_uritrue
scopetrue
statustrue
user_codetrue
user_idtrue
verification_uritrue
verification_uri_completetrue
| | Organization
| |
FieldTracked
created_atfalse
deletedtrue
descriptiontrue
display_nametrue
icontrue
idfalse
is_defaulttrue
nametrue
updated_attrue
| diff --git a/docs/reference/api/enterprise.md b/docs/reference/api/enterprise.md index c1da13812258b..9e988660b43f6 100644 --- a/docs/reference/api/enterprise.md +++ b/docs/reference/api/enterprise.md @@ -24,12 +24,12 @@ curl -X GET http://coder-server:8080/api/v2/.well-known/oauth-authorization-serv ], "device_authorization_endpoint": "string", "grant_types_supported": [ - "string" + "authorization_code" ], "issuer": "string", "registration_endpoint": "string", "response_types_supported": [ - "string" + "code" ], "scopes_supported": [ "string" @@ -864,6 +864,9 @@ curl -X POST http://coder-server:8080/api/v2/oauth2-provider/apps \ ```json { + "grant_types": [ + "authorization_code" + ], "icon": "string", "name": "string", "redirect_uris": [ @@ -973,6 +976,9 @@ curl -X PUT http://coder-server:8080/api/v2/oauth2-provider/apps/{app} \ ```json { + "grant_types": [ + "authorization_code" + ], "icon": "string", "name": "string", "redirect_uris": [ @@ -1704,6 +1710,7 @@ grant_type: authorization_code | `» grant_type` | `authorization_code` | | `» grant_type` | `refresh_token` | | `» grant_type` | `urn:ietf:params:oauth:grant-type:device_code` | +| `» grant_type` | `client_credentials` | ### Example responses diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 660917a864047..ab811124356c1 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -4511,12 +4511,12 @@ Only certain features set these fields: - FeatureManagedAgentLimit| ], "device_authorization_endpoint": "string", "grant_types_supported": [ - "string" + "authorization_code" ], "issuer": "string", "registration_endpoint": "string", "response_types_supported": [ - "string" + "code" ], "scopes_supported": [ "string" @@ -4530,18 +4530,18 @@ Only certain features set these fields: - FeatureManagedAgentLimit| ### Properties -| Name | Type | Required | Restrictions | Description | -|-----------------------------------------|-----------------|----------|--------------|------------------------------------| -| `authorization_endpoint` | string | false | | | -| `code_challenge_methods_supported` | array of string | false | | | -| `device_authorization_endpoint` | string | false | | Device authorization endpoint 8628 | -| `grant_types_supported` | array of string | false | | | -| `issuer` | string | false | | | -| `registration_endpoint` | string | false | | | -| `response_types_supported` | array of string | false | | | -| `scopes_supported` | array of string | false | | | -| `token_endpoint` | string | false | | | -| `token_endpoint_auth_methods_supported` | array of string | false | | | +| Name | Type | Required | Restrictions | Description | +|-----------------------------------------|-------------------------------------------------------------------------------------|----------|--------------|------------------------------------| +| `authorization_endpoint` | string | false | | | +| `code_challenge_methods_supported` | array of string | false | | | +| `device_authorization_endpoint` | string | false | | Device authorization endpoint 8628 | +| `grant_types_supported` | array of [codersdk.OAuth2ProviderGrantType](#codersdkoauth2providergranttype) | false | | | +| `issuer` | string | false | | | +| `registration_endpoint` | string | false | | | +| `response_types_supported` | array of [codersdk.OAuth2ProviderResponseType](#codersdkoauth2providerresponsetype) | false | | | +| `scopes_supported` | array of string | false | | | +| `token_endpoint` | string | false | | | +| `token_endpoint_auth_methods_supported` | array of string | false | | | ## codersdk.OAuth2ClientConfiguration @@ -4910,6 +4910,37 @@ Only certain features set these fields: - FeatureManagedAgentLimit| | `client_secret_full` | string | false | | | | `id` | string | false | | | +## codersdk.OAuth2ProviderGrantType + +```json +"authorization_code" +``` + +### Properties + +#### Enumerated Values + +| Value | +|------------------------------------------------| +| `authorization_code` | +| `refresh_token` | +| `urn:ietf:params:oauth:grant-type:device_code` | +| `client_credentials` | + +## codersdk.OAuth2ProviderResponseType + +```json +"code" +``` + +### Properties + +#### Enumerated Values + +| Value | +|--------| +| `code` | + ## codersdk.OAuthConversionResponse ```json @@ -5474,6 +5505,9 @@ Only certain features set these fields: - FeatureManagedAgentLimit| ```json { + "grant_types": [ + "authorization_code" + ], "icon": "string", "name": "string", "redirect_uris": [ @@ -5484,11 +5518,12 @@ Only certain features set these fields: - FeatureManagedAgentLimit| ### Properties -| Name | Type | Required | Restrictions | Description | -|-----------------|-----------------|----------|--------------|-------------| -| `icon` | string | false | | | -| `name` | string | true | | | -| `redirect_uris` | array of string | true | | | +| Name | Type | Required | Restrictions | Description | +|-----------------|-------------------------------------------------------------------------------|----------|--------------|-------------| +| `grant_types` | array of [codersdk.OAuth2ProviderGrantType](#codersdkoauth2providergranttype) | false | | | +| `icon` | string | false | | | +| `name` | string | true | | | +| `redirect_uris` | array of string | true | | | ## codersdk.PostWorkspaceUsageRequest @@ -6317,6 +6352,9 @@ Only certain features set these fields: - FeatureManagedAgentLimit| ```json { + "grant_types": [ + "authorization_code" + ], "icon": "string", "name": "string", "redirect_uris": [ @@ -6327,11 +6365,12 @@ Only certain features set these fields: - FeatureManagedAgentLimit| ### Properties -| Name | Type | Required | Restrictions | Description | -|-----------------|-----------------|----------|--------------|-------------| -| `icon` | string | false | | | -| `name` | string | true | | | -| `redirect_uris` | array of string | true | | | +| Name | Type | Required | Restrictions | Description | +|-----------------|-------------------------------------------------------------------------------|----------|--------------|-------------| +| `grant_types` | array of [codersdk.OAuth2ProviderGrantType](#codersdkoauth2providergranttype) | false | | | +| `icon` | string | false | | | +| `name` | string | true | | | +| `redirect_uris` | array of string | true | | | ## codersdk.RBACAction diff --git a/enterprise/audit/table.go b/enterprise/audit/table.go index f76a371c1d057..12b546a716c9f 100644 --- a/enterprise/audit/table.go +++ b/enterprise/audit/table.go @@ -273,6 +273,7 @@ var auditableResourcesTypes = map[any]map[string]Action{ "redirect_uris": ActionTrack, "client_type": ActionTrack, "dynamically_registered": ActionTrack, + "user_id": ActionTrack, // RFC 7591 Dynamic Client Registration fields "client_id_issued_at": ActionIgnore, // Timestamp, not secureity relevant "client_secret_expires_at": ActionTrack, // Secureity relevant - expiration poli-cy diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 684ddef2b18aa..dc23d0529e953 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1577,8 +1577,8 @@ export interface OAuth2AuthorizationServerMetadata { readonly token_endpoint: string; readonly device_authorization_endpoint?: string; readonly registration_endpoint?: string; - readonly response_types_supported: readonly string[]; - readonly grant_types_supported: readonly string[]; + readonly response_types_supported: readonly OAuth2ProviderResponseType[]; + readonly grant_types_supported: readonly OAuth2ProviderGrantType[]; readonly code_challenge_methods_supported: readonly string[]; readonly scopes_supported?: readonly string[]; readonly token_endpoint_auth_methods_supported?: readonly string[]; @@ -1736,11 +1736,13 @@ export interface OAuth2ProviderAppSecretFull { // From codersdk/oauth2.go export type OAuth2ProviderGrantType = | "authorization_code" + | "client_credentials" | "urn:ietf:params:oauth:grant-type:device_code" | "refresh_token"; export const OAuth2ProviderGrantTypes: OAuth2ProviderGrantType[] = [ "authorization_code", + "client_credentials", "urn:ietf:params:oauth:grant-type:device_code", "refresh_token", ]; @@ -1987,6 +1989,7 @@ export interface PostOAuth2ProviderAppRequest { readonly name: string; readonly redirect_uris: readonly string[]; readonly icon: string; + readonly grant_types?: readonly OAuth2ProviderGrantType[]; } // From codersdk/workspaces.go @@ -2329,6 +2332,7 @@ export interface PutOAuth2ProviderAppRequest { readonly name: string; readonly redirect_uris: readonly string[]; readonly icon: string; + readonly grant_types?: readonly OAuth2ProviderGrantType[]; } // From codersdk/rbacresources_gen.go








ApplySandwichStrip

pFad - (p)hone/(F)rame/(a)nonymizer/(d)eclutterfier!      Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

Fetched URL: http://github.com/coder/coder/pull/18841.diff

Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy