mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
refactor: add RFC-compliant enum types and use SDK as source of truth (#21468)
Add comprehensive OAuth2 enum types to codersdk following RFC specifications: - OAuth2ProviderGrantType (RFC 6749) - OAuth2ProviderResponseType (RFC 6749) - OAuth2TokenEndpointAuthMethod (RFC 7591) - OAuth2PKCECodeChallengeMethod (RFC 7636) - OAuth2TokenType (RFC 6749, RFC 9449) - OAuth2RevocationTokenTypeHint (RFC 7009) - OAuth2ErrorCode (RFC 6749, RFC 7009, RFC 8707) Add OAuth2TokenRequest, OAuth2TokenResponse, OAuth2TokenRevocationRequest, and OAuth2Error structs to the SDK. Update OAuth2ClientRegistrationRequest, OAuth2ClientRegistrationResponse, OAuth2ClientConfiguration, and OAuth2AuthorizationServerMetadata to use typed enums instead of raw strings. This makes codersdk the single source of truth for OAuth2 types, eliminating duplication between SDK and server-side structs. Closes #21476
This commit is contained in:
Generated
+74
-20
@@ -2628,7 +2628,8 @@ const docTemplate = `{
|
||||
},
|
||||
{
|
||||
"enum": [
|
||||
"code"
|
||||
"code",
|
||||
"token"
|
||||
],
|
||||
"type": "string",
|
||||
"description": "Response type",
|
||||
@@ -2683,7 +2684,8 @@ const docTemplate = `{
|
||||
},
|
||||
{
|
||||
"enum": [
|
||||
"code"
|
||||
"code",
|
||||
"token"
|
||||
],
|
||||
"type": "string",
|
||||
"description": "Response type",
|
||||
@@ -2914,7 +2916,10 @@ const docTemplate = `{
|
||||
{
|
||||
"enum": [
|
||||
"authorization_code",
|
||||
"refresh_token"
|
||||
"refresh_token",
|
||||
"password",
|
||||
"client_credentials",
|
||||
"implicit"
|
||||
],
|
||||
"type": "string",
|
||||
"description": "Grant type",
|
||||
@@ -15844,13 +15849,13 @@ const docTemplate = `{
|
||||
"code_challenge_methods_supported": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/codersdk.OAuth2PKCECodeChallengeMethod"
|
||||
}
|
||||
},
|
||||
"grant_types_supported": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/codersdk.OAuth2ProviderGrantType"
|
||||
}
|
||||
},
|
||||
"issuer": {
|
||||
@@ -15862,7 +15867,7 @@ const docTemplate = `{
|
||||
"response_types_supported": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/codersdk.OAuth2ProviderResponseType"
|
||||
}
|
||||
},
|
||||
"revocation_endpoint": {
|
||||
@@ -15880,7 +15885,7 @@ const docTemplate = `{
|
||||
"token_endpoint_auth_methods_supported": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/codersdk.OAuth2TokenEndpointAuthMethod"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15912,7 +15917,7 @@ const docTemplate = `{
|
||||
"grant_types": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/codersdk.OAuth2ProviderGrantType"
|
||||
}
|
||||
},
|
||||
"jwks": {
|
||||
@@ -15934,10 +15939,7 @@ const docTemplate = `{
|
||||
}
|
||||
},
|
||||
"registration_access_token": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
"type": "string"
|
||||
},
|
||||
"registration_client_uri": {
|
||||
"type": "string"
|
||||
@@ -15945,7 +15947,7 @@ const docTemplate = `{
|
||||
"response_types": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/codersdk.OAuth2ProviderResponseType"
|
||||
}
|
||||
},
|
||||
"scope": {
|
||||
@@ -15958,7 +15960,7 @@ const docTemplate = `{
|
||||
"type": "string"
|
||||
},
|
||||
"token_endpoint_auth_method": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/codersdk.OAuth2TokenEndpointAuthMethod"
|
||||
},
|
||||
"tos_uri": {
|
||||
"type": "string"
|
||||
@@ -15983,7 +15985,7 @@ const docTemplate = `{
|
||||
"grant_types": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/codersdk.OAuth2ProviderGrantType"
|
||||
}
|
||||
},
|
||||
"jwks": {
|
||||
@@ -16007,7 +16009,7 @@ const docTemplate = `{
|
||||
"response_types": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/codersdk.OAuth2ProviderResponseType"
|
||||
}
|
||||
},
|
||||
"scope": {
|
||||
@@ -16023,7 +16025,7 @@ const docTemplate = `{
|
||||
"type": "string"
|
||||
},
|
||||
"token_endpoint_auth_method": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/codersdk.OAuth2TokenEndpointAuthMethod"
|
||||
},
|
||||
"tos_uri": {
|
||||
"type": "string"
|
||||
@@ -16060,7 +16062,7 @@ const docTemplate = `{
|
||||
"grant_types": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/codersdk.OAuth2ProviderGrantType"
|
||||
}
|
||||
},
|
||||
"jwks": {
|
||||
@@ -16090,7 +16092,7 @@ const docTemplate = `{
|
||||
"response_types": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/codersdk.OAuth2ProviderResponseType"
|
||||
}
|
||||
},
|
||||
"scope": {
|
||||
@@ -16103,7 +16105,7 @@ const docTemplate = `{
|
||||
"type": "string"
|
||||
},
|
||||
"token_endpoint_auth_method": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/codersdk.OAuth2TokenEndpointAuthMethod"
|
||||
},
|
||||
"tos_uri": {
|
||||
"type": "string"
|
||||
@@ -16156,6 +16158,17 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.OAuth2PKCECodeChallengeMethod": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"S256",
|
||||
"plain"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"OAuth2PKCECodeChallengeMethodS256",
|
||||
"OAuth2PKCECodeChallengeMethodPlain"
|
||||
]
|
||||
},
|
||||
"codersdk.OAuth2ProtectedResourceMetadata": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -16235,6 +16248,47 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.OAuth2ProviderGrantType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"authorization_code",
|
||||
"refresh_token",
|
||||
"password",
|
||||
"client_credentials",
|
||||
"implicit"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"OAuth2ProviderGrantTypeAuthorizationCode",
|
||||
"OAuth2ProviderGrantTypeRefreshToken",
|
||||
"OAuth2ProviderGrantTypePassword",
|
||||
"OAuth2ProviderGrantTypeClientCredentials",
|
||||
"OAuth2ProviderGrantTypeImplicit"
|
||||
]
|
||||
},
|
||||
"codersdk.OAuth2ProviderResponseType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"code",
|
||||
"token"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"OAuth2ProviderResponseTypeCode",
|
||||
"OAuth2ProviderResponseTypeToken"
|
||||
]
|
||||
},
|
||||
"codersdk.OAuth2TokenEndpointAuthMethod": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"client_secret_basic",
|
||||
"client_secret_post",
|
||||
"none"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"OAuth2TokenEndpointAuthMethodClientSecretBasic",
|
||||
"OAuth2TokenEndpointAuthMethodClientSecretPost",
|
||||
"OAuth2TokenEndpointAuthMethodNone"
|
||||
]
|
||||
},
|
||||
"codersdk.OAuthConversionResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
Generated
+65
-20
@@ -2304,7 +2304,7 @@
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"enum": ["code"],
|
||||
"enum": ["code", "token"],
|
||||
"type": "string",
|
||||
"description": "Response type",
|
||||
"name": "response_type",
|
||||
@@ -2355,7 +2355,7 @@
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"enum": ["code"],
|
||||
"enum": ["code", "token"],
|
||||
"type": "string",
|
||||
"description": "Response type",
|
||||
"name": "response_type",
|
||||
@@ -2555,7 +2555,13 @@
|
||||
"in": "formData"
|
||||
},
|
||||
{
|
||||
"enum": ["authorization_code", "refresh_token"],
|
||||
"enum": [
|
||||
"authorization_code",
|
||||
"refresh_token",
|
||||
"password",
|
||||
"client_credentials",
|
||||
"implicit"
|
||||
],
|
||||
"type": "string",
|
||||
"description": "Grant type",
|
||||
"name": "grant_type",
|
||||
@@ -14353,13 +14359,13 @@
|
||||
"code_challenge_methods_supported": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/codersdk.OAuth2PKCECodeChallengeMethod"
|
||||
}
|
||||
},
|
||||
"grant_types_supported": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/codersdk.OAuth2ProviderGrantType"
|
||||
}
|
||||
},
|
||||
"issuer": {
|
||||
@@ -14371,7 +14377,7 @@
|
||||
"response_types_supported": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/codersdk.OAuth2ProviderResponseType"
|
||||
}
|
||||
},
|
||||
"revocation_endpoint": {
|
||||
@@ -14389,7 +14395,7 @@
|
||||
"token_endpoint_auth_methods_supported": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/codersdk.OAuth2TokenEndpointAuthMethod"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14421,7 +14427,7 @@
|
||||
"grant_types": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/codersdk.OAuth2ProviderGrantType"
|
||||
}
|
||||
},
|
||||
"jwks": {
|
||||
@@ -14443,10 +14449,7 @@
|
||||
}
|
||||
},
|
||||
"registration_access_token": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
"type": "string"
|
||||
},
|
||||
"registration_client_uri": {
|
||||
"type": "string"
|
||||
@@ -14454,7 +14457,7 @@
|
||||
"response_types": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/codersdk.OAuth2ProviderResponseType"
|
||||
}
|
||||
},
|
||||
"scope": {
|
||||
@@ -14467,7 +14470,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"token_endpoint_auth_method": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/codersdk.OAuth2TokenEndpointAuthMethod"
|
||||
},
|
||||
"tos_uri": {
|
||||
"type": "string"
|
||||
@@ -14492,7 +14495,7 @@
|
||||
"grant_types": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/codersdk.OAuth2ProviderGrantType"
|
||||
}
|
||||
},
|
||||
"jwks": {
|
||||
@@ -14516,7 +14519,7 @@
|
||||
"response_types": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/codersdk.OAuth2ProviderResponseType"
|
||||
}
|
||||
},
|
||||
"scope": {
|
||||
@@ -14532,7 +14535,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"token_endpoint_auth_method": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/codersdk.OAuth2TokenEndpointAuthMethod"
|
||||
},
|
||||
"tos_uri": {
|
||||
"type": "string"
|
||||
@@ -14569,7 +14572,7 @@
|
||||
"grant_types": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/codersdk.OAuth2ProviderGrantType"
|
||||
}
|
||||
},
|
||||
"jwks": {
|
||||
@@ -14599,7 +14602,7 @@
|
||||
"response_types": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/codersdk.OAuth2ProviderResponseType"
|
||||
}
|
||||
},
|
||||
"scope": {
|
||||
@@ -14612,7 +14615,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"token_endpoint_auth_method": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/codersdk.OAuth2TokenEndpointAuthMethod"
|
||||
},
|
||||
"tos_uri": {
|
||||
"type": "string"
|
||||
@@ -14665,6 +14668,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.OAuth2PKCECodeChallengeMethod": {
|
||||
"type": "string",
|
||||
"enum": ["S256", "plain"],
|
||||
"x-enum-varnames": [
|
||||
"OAuth2PKCECodeChallengeMethodS256",
|
||||
"OAuth2PKCECodeChallengeMethodPlain"
|
||||
]
|
||||
},
|
||||
"codersdk.OAuth2ProtectedResourceMetadata": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -14744,6 +14755,40 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.OAuth2ProviderGrantType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"authorization_code",
|
||||
"refresh_token",
|
||||
"password",
|
||||
"client_credentials",
|
||||
"implicit"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"OAuth2ProviderGrantTypeAuthorizationCode",
|
||||
"OAuth2ProviderGrantTypeRefreshToken",
|
||||
"OAuth2ProviderGrantTypePassword",
|
||||
"OAuth2ProviderGrantTypeClientCredentials",
|
||||
"OAuth2ProviderGrantTypeImplicit"
|
||||
]
|
||||
},
|
||||
"codersdk.OAuth2ProviderResponseType": {
|
||||
"type": "string",
|
||||
"enum": ["code", "token"],
|
||||
"x-enum-varnames": [
|
||||
"OAuth2ProviderResponseTypeCode",
|
||||
"OAuth2ProviderResponseTypeToken"
|
||||
]
|
||||
},
|
||||
"codersdk.OAuth2TokenEndpointAuthMethod": {
|
||||
"type": "string",
|
||||
"enum": ["client_secret_basic", "client_secret_post", "none"],
|
||||
"x-enum-varnames": [
|
||||
"OAuth2TokenEndpointAuthMethodClientSecretBasic",
|
||||
"OAuth2TokenEndpointAuthMethodClientSecretPost",
|
||||
"OAuth2TokenEndpointAuthMethodNone"
|
||||
]
|
||||
},
|
||||
"codersdk.OAuthConversionResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -493,16 +493,10 @@ func OneWayWebSocketEventSender(rw http.ResponseWriter, r *http.Request) (
|
||||
return sendEvent, closed, nil
|
||||
}
|
||||
|
||||
// OAuth2Error represents an OAuth2-compliant error response per RFC 6749.
|
||||
type OAuth2Error struct {
|
||||
Error string `json:"error"`
|
||||
ErrorDescription string `json:"error_description,omitempty"`
|
||||
}
|
||||
|
||||
// WriteOAuth2Error writes an OAuth2-compliant error response per RFC 6749.
|
||||
// This should be used for all OAuth2 endpoints (/oauth2/*) to ensure compliance.
|
||||
func WriteOAuth2Error(ctx context.Context, rw http.ResponseWriter, status int, errorCode, description string) {
|
||||
Write(ctx, rw, status, OAuth2Error{
|
||||
func WriteOAuth2Error(ctx context.Context, rw http.ResponseWriter, status int, errorCode codersdk.OAuth2ErrorCode, description string) {
|
||||
Write(ctx, rw, status, codersdk.OAuth2Error{
|
||||
Error: errorCode,
|
||||
ErrorDescription: description,
|
||||
})
|
||||
|
||||
@@ -290,15 +290,15 @@ func (*codersdkErrorWriter) writeClientNotFound(ctx context.Context, rw http.Res
|
||||
type oauth2ErrorWriter struct{}
|
||||
|
||||
func (*oauth2ErrorWriter) writeMissingClientID(ctx context.Context, rw http.ResponseWriter) {
|
||||
httpapi.WriteOAuth2Error(ctx, rw, http.StatusBadRequest, "invalid_request", "Missing client_id parameter")
|
||||
httpapi.WriteOAuth2Error(ctx, rw, http.StatusBadRequest, codersdk.OAuth2ErrorCodeInvalidRequest, "Missing client_id parameter")
|
||||
}
|
||||
|
||||
func (*oauth2ErrorWriter) writeInvalidClientID(ctx context.Context, rw http.ResponseWriter, _ error) {
|
||||
httpapi.WriteOAuth2Error(ctx, rw, http.StatusUnauthorized, "invalid_client", "The client credentials are invalid")
|
||||
httpapi.WriteOAuth2Error(ctx, rw, http.StatusUnauthorized, codersdk.OAuth2ErrorCodeInvalidClient, "The client credentials are invalid")
|
||||
}
|
||||
|
||||
func (*oauth2ErrorWriter) writeClientNotFound(ctx context.Context, rw http.ResponseWriter) {
|
||||
httpapi.WriteOAuth2Error(ctx, rw, http.StatusUnauthorized, "invalid_client", "The client credentials are invalid")
|
||||
httpapi.WriteOAuth2Error(ctx, rw, http.StatusUnauthorized, codersdk.OAuth2ErrorCodeInvalidClient, "The client credentials are invalid")
|
||||
}
|
||||
|
||||
// extractOAuth2ProviderAppBase is the internal implementation that uses the strategy pattern
|
||||
|
||||
@@ -99,7 +99,7 @@ func TestOAuth2RegistrationErrorCodes(t *testing.T) {
|
||||
req: codersdk.OAuth2ClientRegistrationRequest{
|
||||
RedirectURIs: []string{"https://example.com/callback"},
|
||||
ClientName: fmt.Sprintf("test-client-%d", time.Now().UnixNano()),
|
||||
GrantTypes: []string{"unsupported_grant_type"},
|
||||
GrantTypes: []codersdk.OAuth2ProviderGrantType{"unsupported_grant_type"},
|
||||
},
|
||||
expectedError: "invalid_client_metadata",
|
||||
expectedCode: http.StatusBadRequest,
|
||||
@@ -109,7 +109,7 @@ func TestOAuth2RegistrationErrorCodes(t *testing.T) {
|
||||
req: codersdk.OAuth2ClientRegistrationRequest{
|
||||
RedirectURIs: []string{"https://example.com/callback"},
|
||||
ClientName: fmt.Sprintf("test-client-%d", time.Now().UnixNano()),
|
||||
ResponseTypes: []string{"unsupported_response_type"},
|
||||
ResponseTypes: []codersdk.OAuth2ProviderResponseType{"unsupported_response_type"},
|
||||
},
|
||||
expectedError: "invalid_client_metadata",
|
||||
expectedCode: http.StatusBadRequest,
|
||||
|
||||
@@ -44,10 +44,10 @@ 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.CodeChallengeMethodsSupported, "S256")
|
||||
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, codersdk.OAuth2PKCECodeChallengeMethodS256)
|
||||
// Supported scopes are published from the curated catalog
|
||||
require.Equal(t, rbac.ExternalScopeNames(), metadata.ScopesSupported)
|
||||
}
|
||||
|
||||
@@ -277,47 +277,47 @@ func TestOAuth2ClientMetadataValidation(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
grantTypes []string
|
||||
grantTypes []codersdk.OAuth2ProviderGrantType
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "DefaultEmpty",
|
||||
grantTypes: []string{},
|
||||
grantTypes: []codersdk.OAuth2ProviderGrantType{},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "ValidAuthorizationCode",
|
||||
grantTypes: []string{"authorization_code"},
|
||||
grantTypes: []codersdk.OAuth2ProviderGrantType{"authorization_code"},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "InvalidRefreshTokenAlone",
|
||||
grantTypes: []string{"refresh_token"},
|
||||
grantTypes: []codersdk.OAuth2ProviderGrantType{"refresh_token"},
|
||||
expectError: true, // refresh_token requires authorization_code to be present
|
||||
},
|
||||
{
|
||||
name: "ValidMultiple",
|
||||
grantTypes: []string{"authorization_code", "refresh_token"},
|
||||
grantTypes: []codersdk.OAuth2ProviderGrantType{"authorization_code", "refresh_token"},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "InvalidUnsupported",
|
||||
grantTypes: []string{"client_credentials"},
|
||||
grantTypes: []codersdk.OAuth2ProviderGrantType{"client_credentials"},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "InvalidPassword",
|
||||
grantTypes: []string{"password"},
|
||||
grantTypes: []codersdk.OAuth2ProviderGrantType{"password"},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "InvalidImplicit",
|
||||
grantTypes: []string{"implicit"},
|
||||
grantTypes: []codersdk.OAuth2ProviderGrantType{"implicit"},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "MixedValidInvalid",
|
||||
grantTypes: []string{"authorization_code", "client_credentials"},
|
||||
grantTypes: []codersdk.OAuth2ProviderGrantType{"authorization_code", "client_credentials"},
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
@@ -352,32 +352,32 @@ func TestOAuth2ClientMetadataValidation(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
responseTypes []string
|
||||
responseTypes []codersdk.OAuth2ProviderResponseType
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "DefaultEmpty",
|
||||
responseTypes: []string{},
|
||||
responseTypes: []codersdk.OAuth2ProviderResponseType{},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "ValidCode",
|
||||
responseTypes: []string{"code"},
|
||||
responseTypes: []codersdk.OAuth2ProviderResponseType{"code"},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "InvalidToken",
|
||||
responseTypes: []string{"token"},
|
||||
responseTypes: []codersdk.OAuth2ProviderResponseType{"token"},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "InvalidImplicit",
|
||||
responseTypes: []string{"id_token"},
|
||||
responseTypes: []codersdk.OAuth2ProviderResponseType{"id_token"},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "InvalidMultiple",
|
||||
responseTypes: []string{"code", "token"},
|
||||
responseTypes: []codersdk.OAuth2ProviderResponseType{"code", "token"},
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
@@ -412,7 +412,7 @@ func TestOAuth2ClientMetadataValidation(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
authMethod string
|
||||
authMethod codersdk.OAuth2TokenEndpointAuthMethod
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
@@ -659,14 +659,14 @@ func TestOAuth2ClientMetadataDefaults(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should default to authorization_code
|
||||
require.Contains(t, config.GrantTypes, "authorization_code")
|
||||
require.Contains(t, config.GrantTypes, codersdk.OAuth2ProviderGrantTypeAuthorizationCode)
|
||||
|
||||
// Should default to code
|
||||
require.Contains(t, config.ResponseTypes, "code")
|
||||
require.Contains(t, config.ResponseTypes, codersdk.OAuth2ProviderResponseTypeCode)
|
||||
|
||||
// Should default to client_secret_basic or client_secret_post
|
||||
require.True(t, config.TokenEndpointAuthMethod == "client_secret_basic" ||
|
||||
config.TokenEndpointAuthMethod == "client_secret_post" ||
|
||||
require.True(t, config.TokenEndpointAuthMethod == codersdk.OAuth2TokenEndpointAuthMethodClientSecretBasic ||
|
||||
config.TokenEndpointAuthMethod == codersdk.OAuth2TokenEndpointAuthMethodClientSecretPost ||
|
||||
config.TokenEndpointAuthMethod == "")
|
||||
|
||||
// Client secret should be generated
|
||||
|
||||
@@ -1329,10 +1329,10 @@ func TestOAuth2DynamicClientRegistration(t *testing.T) {
|
||||
require.Equal(t, int64(0), resp.ClientSecretExpiresAt) // Non-expiring
|
||||
|
||||
// Verify default values
|
||||
require.Contains(t, resp.GrantTypes, "authorization_code")
|
||||
require.Contains(t, resp.GrantTypes, "refresh_token")
|
||||
require.Contains(t, resp.ResponseTypes, "code")
|
||||
require.Equal(t, "client_secret_basic", resp.TokenEndpointAuthMethod)
|
||||
require.Contains(t, resp.GrantTypes, codersdk.OAuth2ProviderGrantTypeAuthorizationCode)
|
||||
require.Contains(t, resp.GrantTypes, codersdk.OAuth2ProviderGrantTypeRefreshToken)
|
||||
require.Contains(t, resp.ResponseTypes, codersdk.OAuth2ProviderResponseTypeCode)
|
||||
require.Equal(t, codersdk.OAuth2TokenEndpointAuthMethodClientSecretBasic, resp.TokenEndpointAuthMethod)
|
||||
|
||||
// Verify request values are preserved
|
||||
require.Equal(t, req.RedirectURIs, resp.RedirectURIs)
|
||||
@@ -1363,9 +1363,9 @@ func TestOAuth2DynamicClientRegistration(t *testing.T) {
|
||||
require.NotEmpty(t, resp.RegistrationClientURI)
|
||||
|
||||
// Should have defaults applied
|
||||
require.Contains(t, resp.GrantTypes, "authorization_code")
|
||||
require.Contains(t, resp.ResponseTypes, "code")
|
||||
require.Equal(t, "client_secret_basic", resp.TokenEndpointAuthMethod)
|
||||
require.Contains(t, resp.GrantTypes, codersdk.OAuth2ProviderGrantTypeAuthorizationCode)
|
||||
require.Contains(t, resp.ResponseTypes, codersdk.OAuth2ProviderResponseTypeCode)
|
||||
require.Equal(t, codersdk.OAuth2TokenEndpointAuthMethodClientSecretBasic, resp.TokenEndpointAuthMethod)
|
||||
})
|
||||
|
||||
t.Run("InvalidRedirectURI", func(t *testing.T) {
|
||||
|
||||
@@ -137,13 +137,13 @@ func ProcessAuthorize(db database.Store) http.HandlerFunc {
|
||||
|
||||
callbackURL, err := url.Parse(app.CallbackURL)
|
||||
if err != nil {
|
||||
httpapi.WriteOAuth2Error(r.Context(), rw, http.StatusInternalServerError, "server_error", "Failed to validate query parameters")
|
||||
httpapi.WriteOAuth2Error(r.Context(), rw, http.StatusInternalServerError, codersdk.OAuth2ErrorCodeServerError, "Failed to validate query parameters")
|
||||
return
|
||||
}
|
||||
|
||||
params, _, err := extractAuthorizeParams(r, callbackURL)
|
||||
if err != nil {
|
||||
httpapi.WriteOAuth2Error(ctx, rw, http.StatusBadRequest, "invalid_request", err.Error())
|
||||
httpapi.WriteOAuth2Error(ctx, rw, http.StatusBadRequest, codersdk.OAuth2ErrorCodeInvalidRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -151,10 +151,10 @@ func ProcessAuthorize(db database.Store) http.HandlerFunc {
|
||||
if params.codeChallenge != "" {
|
||||
// If code_challenge is provided but method is not, default to S256
|
||||
if params.codeChallengeMethod == "" {
|
||||
params.codeChallengeMethod = "S256"
|
||||
params.codeChallengeMethod = string(codersdk.OAuth2PKCECodeChallengeMethodS256)
|
||||
}
|
||||
if params.codeChallengeMethod != "S256" {
|
||||
httpapi.WriteOAuth2Error(ctx, rw, http.StatusBadRequest, "invalid_request", "Invalid code_challenge_method: only S256 is supported")
|
||||
if err := codersdk.ValidatePKCECodeChallengeMethod(params.codeChallengeMethod); err != nil {
|
||||
httpapi.WriteOAuth2Error(ctx, rw, http.StatusBadRequest, codersdk.OAuth2ErrorCodeInvalidRequest, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -162,7 +162,7 @@ func ProcessAuthorize(db database.Store) http.HandlerFunc {
|
||||
// TODO: Ignoring scope for now, but should look into implementing.
|
||||
code, err := GenerateSecret()
|
||||
if err != nil {
|
||||
httpapi.WriteOAuth2Error(r.Context(), rw, http.StatusInternalServerError, "server_error", "Failed to generate OAuth2 app authorization code")
|
||||
httpapi.WriteOAuth2Error(r.Context(), rw, http.StatusInternalServerError, codersdk.OAuth2ErrorCodeServerError, "Failed to generate OAuth2 app authorization code")
|
||||
return
|
||||
}
|
||||
err = db.InTx(func(tx database.Store) error {
|
||||
@@ -202,7 +202,7 @@ func ProcessAuthorize(db database.Store) http.HandlerFunc {
|
||||
return nil
|
||||
}, nil)
|
||||
if err != nil {
|
||||
httpapi.WriteOAuth2Error(ctx, rw, http.StatusInternalServerError, "server_error", "Failed to generate OAuth2 authorization code")
|
||||
httpapi.WriteOAuth2Error(ctx, rw, http.StatusInternalServerError, codersdk.OAuth2ErrorCodeServerError, "Failed to generate OAuth2 authorization code")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -19,11 +19,11 @@ func GetAuthorizationServerMetadata(accessURL *url.URL) http.HandlerFunc {
|
||||
TokenEndpoint: accessURL.JoinPath("/oauth2/tokens").String(),
|
||||
RegistrationEndpoint: accessURL.JoinPath("/oauth2/register").String(), // RFC 7591
|
||||
RevocationEndpoint: accessURL.JoinPath("/oauth2/revoke").String(), // RFC 7009
|
||||
ResponseTypesSupported: []string{"code"},
|
||||
GrantTypesSupported: []string{"authorization_code", "refresh_token"},
|
||||
CodeChallengeMethodsSupported: []string{"S256"},
|
||||
ResponseTypesSupported: []codersdk.OAuth2ProviderResponseType{codersdk.OAuth2ProviderResponseTypeCode},
|
||||
GrantTypesSupported: []codersdk.OAuth2ProviderGrantType{codersdk.OAuth2ProviderGrantTypeAuthorizationCode, codersdk.OAuth2ProviderGrantTypeRefreshToken},
|
||||
CodeChallengeMethodsSupported: []codersdk.OAuth2PKCECodeChallengeMethod{codersdk.OAuth2PKCECodeChallengeMethodS256},
|
||||
ScopesSupported: rbac.ExternalScopeNames(),
|
||||
TokenEndpointAuthMethodsSupported: []string{"client_secret_post"},
|
||||
TokenEndpointAuthMethodsSupported: []codersdk.OAuth2TokenEndpointAuthMethod{codersdk.OAuth2TokenEndpointAuthMethodClientSecretPost},
|
||||
}
|
||||
httpapi.Write(ctx, rw, http.StatusOK, metadata)
|
||||
}
|
||||
|
||||
@@ -32,10 +32,10 @@ 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.CodeChallengeMethodsSupported, "S256")
|
||||
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, codersdk.OAuth2PKCECodeChallengeMethodS256)
|
||||
// Supported scopes are published from the curated catalog
|
||||
require.Equal(t, rbac.ExternalScopeNames(), metadata.ScopesSupported)
|
||||
}
|
||||
|
||||
@@ -105,8 +105,9 @@ func GenerateState(t *testing.T) string {
|
||||
return base64.RawURLEncoding.EncodeToString(bytes)
|
||||
}
|
||||
|
||||
// AuthorizeOAuth2App performs the OAuth2 authorization flow and returns the authorization code
|
||||
func AuthorizeOAuth2App(t *testing.T, client *codersdk.Client, baseURL string, params AuthorizeParams) string {
|
||||
// doAuthorizeRequest performs the OAuth2 authorization request and returns the response.
|
||||
// Caller is responsible for closing the response body.
|
||||
func doAuthorizeRequest(t *testing.T, client *codersdk.Client, baseURL string, params AuthorizeParams) *http.Response {
|
||||
t.Helper()
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
@@ -123,6 +124,8 @@ func AuthorizeOAuth2App(t *testing.T, client *codersdk.Client, baseURL string, p
|
||||
|
||||
if params.CodeChallenge != "" {
|
||||
query.Set("code_challenge", params.CodeChallenge)
|
||||
}
|
||||
if params.CodeChallengeMethod != "" {
|
||||
query.Set("code_challenge_method", params.CodeChallengeMethod)
|
||||
}
|
||||
if params.Resource != "" {
|
||||
@@ -151,6 +154,15 @@ func AuthorizeOAuth2App(t *testing.T, client *codersdk.Client, baseURL string, p
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
require.NoError(t, err, "failed to perform authorization request")
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
// AuthorizeOAuth2App performs the OAuth2 authorization flow and returns the authorization code
|
||||
func AuthorizeOAuth2App(t *testing.T, client *codersdk.Client, baseURL string, params AuthorizeParams) string {
|
||||
t.Helper()
|
||||
|
||||
resp := doAuthorizeRequest(t, client, baseURL, params)
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Should get a redirect response (either 302 Found or 307 Temporary Redirect)
|
||||
@@ -326,3 +338,13 @@ func CleanupOAuth2App(t *testing.T, client *codersdk.Client, appID uuid.UUID) {
|
||||
t.Logf("Warning: failed to cleanup OAuth2 app %s: %v", appID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// AuthorizeOAuth2AppExpectingError performs the OAuth2 authorization flow expecting an error
|
||||
func AuthorizeOAuth2AppExpectingError(t *testing.T, client *codersdk.Client, baseURL string, params AuthorizeParams, expectedStatusCode int) {
|
||||
t.Helper()
|
||||
|
||||
resp := doAuthorizeRequest(t, client, baseURL, params)
|
||||
defer resp.Body.Close()
|
||||
|
||||
require.Equal(t, expectedStatusCode, resp.StatusCode, "unexpected status code")
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||
"github.com/coder/coder/v2/coderd/oauth2provider/oauth2providertest"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
func TestOAuth2AuthorizationServerMetadata(t *testing.T) {
|
||||
@@ -185,6 +186,38 @@ func TestOAuth2WithoutPKCE(t *testing.T) {
|
||||
require.NotEmpty(t, token.RefreshToken, "should receive refresh token")
|
||||
}
|
||||
|
||||
func TestOAuth2PKCEPlainMethodRejected(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := coderdtest.New(t, &coderdtest.Options{
|
||||
IncludeProvisionerDaemon: false,
|
||||
})
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
|
||||
// Create OAuth2 app
|
||||
app, _ := oauth2providertest.CreateTestOAuth2App(t, client)
|
||||
t.Cleanup(func() {
|
||||
oauth2providertest.CleanupOAuth2App(t, client, app.ID)
|
||||
})
|
||||
|
||||
// Generate PKCE parameters but use "plain" method (should be rejected)
|
||||
_, codeChallenge := oauth2providertest.GeneratePKCE(t)
|
||||
state := oauth2providertest.GenerateState(t)
|
||||
|
||||
// Attempt authorization with plain method - should fail
|
||||
authParams := oauth2providertest.AuthorizeParams{
|
||||
ClientID: app.ID.String(),
|
||||
ResponseType: string(codersdk.OAuth2ProviderResponseTypeCode),
|
||||
RedirectURI: oauth2providertest.TestRedirectURI,
|
||||
State: state,
|
||||
CodeChallenge: codeChallenge,
|
||||
CodeChallengeMethod: string(codersdk.OAuth2PKCECodeChallengeMethodPlain),
|
||||
}
|
||||
|
||||
// Should get a 400 Bad Request
|
||||
oauth2providertest.AuthorizeOAuth2AppExpectingError(t, client, client.URL.String(), authParams, 400)
|
||||
}
|
||||
|
||||
func TestOAuth2ResourceParameter(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/oauth2provider"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
func TestVerifyPKCE(t *testing.T) {
|
||||
@@ -75,3 +76,52 @@ func TestPKCES256Generation(t *testing.T) {
|
||||
require.Equal(t, expectedChallenge, challenge)
|
||||
require.True(t, oauth2provider.VerifyPKCE(challenge, verifier))
|
||||
}
|
||||
|
||||
func TestValidatePKCECodeChallengeMethod(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
method string
|
||||
expectError bool
|
||||
errorContains string
|
||||
}{
|
||||
{
|
||||
name: "EmptyIsValid",
|
||||
method: "",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "S256IsValid",
|
||||
method: string(codersdk.OAuth2PKCECodeChallengeMethodS256),
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "PlainIsRejected",
|
||||
method: string(codersdk.OAuth2PKCECodeChallengeMethodPlain),
|
||||
expectError: true,
|
||||
errorContains: "plain",
|
||||
},
|
||||
{
|
||||
name: "UnknownIsRejected",
|
||||
method: "unknown_method",
|
||||
expectError: true,
|
||||
errorContains: "unsupported",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
err := codersdk.ValidatePKCECodeChallengeMethod(tt.method)
|
||||
if tt.expectError {
|
||||
require.Error(t, err)
|
||||
if tt.errorContains != "" {
|
||||
require.Contains(t, err.Error(), tt.errorContains)
|
||||
}
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,7 +248,7 @@ func TestOAuth2ClientRegistrationValidation(t *testing.T) {
|
||||
req := codersdk.OAuth2ClientRegistrationRequest{
|
||||
RedirectURIs: []string{"https://example.com/callback"},
|
||||
ClientName: fmt.Sprintf("valid-grant-types-client-%d", time.Now().UnixNano()),
|
||||
GrantTypes: []string{"authorization_code", "refresh_token"},
|
||||
GrantTypes: []codersdk.OAuth2ProviderGrantType{codersdk.OAuth2ProviderGrantTypeAuthorizationCode, codersdk.OAuth2ProviderGrantTypeRefreshToken},
|
||||
}
|
||||
|
||||
resp, err := client.PostOAuth2ClientRegistration(ctx, req)
|
||||
@@ -266,7 +266,7 @@ func TestOAuth2ClientRegistrationValidation(t *testing.T) {
|
||||
req := codersdk.OAuth2ClientRegistrationRequest{
|
||||
RedirectURIs: []string{"https://example.com/callback"},
|
||||
ClientName: fmt.Sprintf("invalid-grant-types-client-%d", time.Now().UnixNano()),
|
||||
GrantTypes: []string{"unsupported_grant"},
|
||||
GrantTypes: []codersdk.OAuth2ProviderGrantType{"unsupported_grant"},
|
||||
}
|
||||
|
||||
_, err := client.PostOAuth2ClientRegistration(ctx, req)
|
||||
@@ -284,7 +284,7 @@ func TestOAuth2ClientRegistrationValidation(t *testing.T) {
|
||||
req := codersdk.OAuth2ClientRegistrationRequest{
|
||||
RedirectURIs: []string{"https://example.com/callback"},
|
||||
ClientName: fmt.Sprintf("valid-response-types-client-%d", time.Now().UnixNano()),
|
||||
ResponseTypes: []string{"code"},
|
||||
ResponseTypes: []codersdk.OAuth2ProviderResponseType{codersdk.OAuth2ProviderResponseTypeCode},
|
||||
}
|
||||
|
||||
resp, err := client.PostOAuth2ClientRegistration(ctx, req)
|
||||
@@ -302,7 +302,7 @@ func TestOAuth2ClientRegistrationValidation(t *testing.T) {
|
||||
req := codersdk.OAuth2ClientRegistrationRequest{
|
||||
RedirectURIs: []string{"https://example.com/callback"},
|
||||
ClientName: fmt.Sprintf("invalid-response-types-client-%d", time.Now().UnixNano()),
|
||||
ResponseTypes: []string{"token"}, // Not supported
|
||||
ResponseTypes: []codersdk.OAuth2ProviderResponseType{"token"}, // Not supported
|
||||
}
|
||||
|
||||
_, err := client.PostOAuth2ClientRegistration(ctx, req)
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||
"github.com/coder/coder/v2/coderd/database/dbtime"
|
||||
"github.com/coder/coder/v2/coderd/httpapi"
|
||||
"github.com/coder/coder/v2/coderd/util/slice"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
@@ -85,9 +86,9 @@ func CreateDynamicClientRegistration(db database.Store, accessURL *url.URL, audi
|
||||
DynamicallyRegistered: sql.NullBool{Bool: true, Valid: true},
|
||||
ClientIDIssuedAt: sql.NullTime{Time: now, Valid: true},
|
||||
ClientSecretExpiresAt: sql.NullTime{}, // No expiration for now
|
||||
GrantTypes: req.GrantTypes,
|
||||
ResponseTypes: req.ResponseTypes,
|
||||
TokenEndpointAuthMethod: sql.NullString{String: req.TokenEndpointAuthMethod, Valid: true},
|
||||
GrantTypes: slice.ToStrings(req.GrantTypes),
|
||||
ResponseTypes: slice.ToStrings(req.ResponseTypes),
|
||||
TokenEndpointAuthMethod: sql.NullString{String: string(req.TokenEndpointAuthMethod), Valid: true},
|
||||
Scope: sql.NullString{String: req.Scope, Valid: true},
|
||||
Contacts: req.Contacts,
|
||||
ClientUri: sql.NullString{String: req.ClientURI, Valid: req.ClientURI != ""},
|
||||
@@ -154,9 +155,9 @@ func CreateDynamicClientRegistration(db database.Store, accessURL *url.URL, audi
|
||||
JWKS: app.Jwks.RawMessage,
|
||||
SoftwareID: app.SoftwareID.String,
|
||||
SoftwareVersion: app.SoftwareVersion.String,
|
||||
GrantTypes: app.GrantTypes,
|
||||
ResponseTypes: app.ResponseTypes,
|
||||
TokenEndpointAuthMethod: app.TokenEndpointAuthMethod.String,
|
||||
GrantTypes: slice.StringEnums[codersdk.OAuth2ProviderGrantType](app.GrantTypes),
|
||||
ResponseTypes: slice.StringEnums[codersdk.OAuth2ProviderResponseType](app.ResponseTypes),
|
||||
TokenEndpointAuthMethod: codersdk.OAuth2TokenEndpointAuthMethod(app.TokenEndpointAuthMethod.String),
|
||||
Scope: app.Scope.String,
|
||||
Contacts: app.Contacts,
|
||||
RegistrationAccessToken: registrationToken,
|
||||
@@ -217,12 +218,12 @@ func GetClientConfiguration(db database.Store) http.HandlerFunc {
|
||||
JWKS: app.Jwks.RawMessage,
|
||||
SoftwareID: app.SoftwareID.String,
|
||||
SoftwareVersion: app.SoftwareVersion.String,
|
||||
GrantTypes: app.GrantTypes,
|
||||
ResponseTypes: app.ResponseTypes,
|
||||
TokenEndpointAuthMethod: app.TokenEndpointAuthMethod.String,
|
||||
GrantTypes: slice.StringEnums[codersdk.OAuth2ProviderGrantType](app.GrantTypes),
|
||||
ResponseTypes: slice.StringEnums[codersdk.OAuth2ProviderResponseType](app.ResponseTypes),
|
||||
TokenEndpointAuthMethod: codersdk.OAuth2TokenEndpointAuthMethod(app.TokenEndpointAuthMethod.String),
|
||||
Scope: app.Scope.String,
|
||||
Contacts: app.Contacts,
|
||||
RegistrationAccessToken: nil, // RFC 7592: Not returned in GET responses for security
|
||||
RegistrationAccessToken: "", // RFC 7592: Not returned in GET responses for security
|
||||
RegistrationClientURI: app.RegistrationClientUri.String,
|
||||
}
|
||||
|
||||
@@ -303,9 +304,9 @@ func UpdateClientConfiguration(db database.Store, auditor *audit.Auditor, logger
|
||||
RedirectUris: req.RedirectURIs,
|
||||
ClientType: sql.NullString{String: req.DetermineClientType(), Valid: true},
|
||||
ClientSecretExpiresAt: sql.NullTime{}, // No expiration for now
|
||||
GrantTypes: req.GrantTypes,
|
||||
ResponseTypes: req.ResponseTypes,
|
||||
TokenEndpointAuthMethod: sql.NullString{String: req.TokenEndpointAuthMethod, Valid: true},
|
||||
GrantTypes: slice.ToStrings(req.GrantTypes),
|
||||
ResponseTypes: slice.ToStrings(req.ResponseTypes),
|
||||
TokenEndpointAuthMethod: sql.NullString{String: string(req.TokenEndpointAuthMethod), Valid: true},
|
||||
Scope: sql.NullString{String: req.Scope, Valid: true},
|
||||
Contacts: req.Contacts,
|
||||
ClientUri: sql.NullString{String: req.ClientURI, Valid: req.ClientURI != ""},
|
||||
@@ -341,12 +342,12 @@ func UpdateClientConfiguration(db database.Store, auditor *audit.Auditor, logger
|
||||
JWKS: updatedApp.Jwks.RawMessage,
|
||||
SoftwareID: updatedApp.SoftwareID.String,
|
||||
SoftwareVersion: updatedApp.SoftwareVersion.String,
|
||||
GrantTypes: updatedApp.GrantTypes,
|
||||
ResponseTypes: updatedApp.ResponseTypes,
|
||||
TokenEndpointAuthMethod: updatedApp.TokenEndpointAuthMethod.String,
|
||||
GrantTypes: slice.StringEnums[codersdk.OAuth2ProviderGrantType](updatedApp.GrantTypes),
|
||||
ResponseTypes: slice.StringEnums[codersdk.OAuth2ProviderResponseType](updatedApp.ResponseTypes),
|
||||
TokenEndpointAuthMethod: codersdk.OAuth2TokenEndpointAuthMethod(updatedApp.TokenEndpointAuthMethod.String),
|
||||
Scope: updatedApp.Scope.String,
|
||||
Contacts: updatedApp.Contacts,
|
||||
RegistrationAccessToken: updatedApp.RegistrationAccessToken,
|
||||
RegistrationAccessToken: "", // RFC 7592: Not returned for security
|
||||
RegistrationClientURI: updatedApp.RegistrationClientUri.String,
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||
"github.com/coder/coder/v2/coderd/httpapi"
|
||||
"github.com/coder/coder/v2/coderd/httpmw"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -27,6 +28,26 @@ var (
|
||||
ErrInvalidTokenFormat = xerrors.New("invalid token format")
|
||||
)
|
||||
|
||||
func extractRevocationRequest(r *http.Request) (codersdk.OAuth2TokenRevocationRequest, error) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
return codersdk.OAuth2TokenRevocationRequest{}, xerrors.Errorf("invalid form data: %w", err)
|
||||
}
|
||||
|
||||
req := codersdk.OAuth2TokenRevocationRequest{
|
||||
Token: r.Form.Get("token"),
|
||||
TokenTypeHint: codersdk.OAuth2RevocationTokenTypeHint(r.Form.Get("token_type_hint")),
|
||||
ClientID: r.Form.Get("client_id"),
|
||||
ClientSecret: r.Form.Get("client_secret"),
|
||||
}
|
||||
|
||||
// RFC 7009 requires 'token' parameter.
|
||||
if req.Token == "" {
|
||||
return codersdk.OAuth2TokenRevocationRequest{}, xerrors.New("missing token parameter")
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// RevokeToken implements RFC 7009 OAuth2 Token Revocation
|
||||
// Authentication is unique for this endpoint in that it does not use the
|
||||
// standard token authentication middleware. Instead, it expects the token that
|
||||
@@ -41,35 +62,29 @@ func RevokeToken(db database.Store, logger slog.Logger) http.HandlerFunc {
|
||||
|
||||
// RFC 7009 requires POST method with application/x-www-form-urlencoded
|
||||
if r.Method != http.MethodPost {
|
||||
httpapi.WriteOAuth2Error(ctx, rw, http.StatusMethodNotAllowed, "invalid_request", "Method not allowed")
|
||||
httpapi.WriteOAuth2Error(ctx, rw, http.StatusMethodNotAllowed, codersdk.OAuth2ErrorCodeInvalidRequest, "Method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
if err := r.ParseForm(); err != nil {
|
||||
httpapi.WriteOAuth2Error(ctx, rw, http.StatusBadRequest, "invalid_request", "Invalid form data")
|
||||
return
|
||||
}
|
||||
|
||||
// RFC 7009 requires 'token' parameter
|
||||
token := r.Form.Get("token")
|
||||
if token == "" {
|
||||
httpapi.WriteOAuth2Error(ctx, rw, http.StatusBadRequest, "invalid_request", "Missing token parameter")
|
||||
req, err := extractRevocationRequest(r)
|
||||
if err != nil {
|
||||
httpapi.WriteOAuth2Error(ctx, rw, http.StatusBadRequest, codersdk.OAuth2ErrorCodeInvalidRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Determine if this is a refresh token (starts with "coder_") or API key
|
||||
// APIKeys do not have the SecretIdentifier prefix.
|
||||
const coderPrefix = SecretIdentifier + "_"
|
||||
isRefreshToken := strings.HasPrefix(token, coderPrefix)
|
||||
isRefreshToken := strings.HasPrefix(req.Token, coderPrefix)
|
||||
|
||||
// Revoke the token with ownership verification
|
||||
err := db.InTx(func(tx database.Store) error {
|
||||
err = db.InTx(func(tx database.Store) error {
|
||||
if isRefreshToken {
|
||||
// Handle refresh token revocation
|
||||
return revokeRefreshTokenInTx(ctx, tx, token, app.ID)
|
||||
return revokeRefreshTokenInTx(ctx, tx, req.Token, app.ID)
|
||||
}
|
||||
// Handle API key revocation
|
||||
return revokeAPIKeyInTx(ctx, tx, token, app.ID)
|
||||
return revokeAPIKeyInTx(ctx, tx, req.Token, app.ID)
|
||||
}, nil)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrTokenNotBelongsToClient) {
|
||||
@@ -85,14 +100,14 @@ func RevokeToken(db database.Store, logger slog.Logger) http.HandlerFunc {
|
||||
logger.Debug(ctx, "token revocation failed: invalid token format",
|
||||
slog.F("client_id", app.ID.String()),
|
||||
slog.F("app_name", app.Name))
|
||||
httpapi.WriteOAuth2Error(ctx, rw, http.StatusBadRequest, "invalid_request", "Invalid token format")
|
||||
httpapi.WriteOAuth2Error(ctx, rw, http.StatusBadRequest, codersdk.OAuth2ErrorCodeInvalidRequest, "Invalid token format")
|
||||
return
|
||||
}
|
||||
logger.Error(ctx, "token revocation failed with internal server error",
|
||||
slog.Error(err),
|
||||
slog.F("client_id", app.ID.String()),
|
||||
slog.F("app_name", app.Name))
|
||||
httpapi.WriteOAuth2Error(ctx, rw, http.StatusInternalServerError, "server_error", "Internal server error")
|
||||
httpapi.WriteOAuth2Error(ctx, rw, http.StatusInternalServerError, codersdk.OAuth2ErrorCodeServerError, "Internal server error")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -8,11 +8,9 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/apikey"
|
||||
@@ -38,28 +36,18 @@ var (
|
||||
errInvalidResource = xerrors.New("invalid resource parameter")
|
||||
)
|
||||
|
||||
type tokenParams struct {
|
||||
clientID string
|
||||
clientSecret string
|
||||
code string
|
||||
grantType codersdk.OAuth2ProviderGrantType
|
||||
redirectURL *url.URL
|
||||
refreshToken string
|
||||
codeVerifier string // PKCE verifier
|
||||
resource string // RFC 8707 resource for token binding
|
||||
scopes []string
|
||||
}
|
||||
|
||||
func extractTokenParams(r *http.Request, callbackURL *url.URL) (tokenParams, []codersdk.ValidationError, error) {
|
||||
func extractTokenRequest(r *http.Request, callbackURL *url.URL) (codersdk.OAuth2TokenRequest, []codersdk.ValidationError, error) {
|
||||
p := httpapi.NewQueryParamParser()
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
return tokenParams{}, nil, xerrors.Errorf("parse form: %w", err)
|
||||
return codersdk.OAuth2TokenRequest{}, nil, xerrors.Errorf("parse form: %w", err)
|
||||
}
|
||||
|
||||
vals := r.Form
|
||||
p.RequiredNotEmpty("grant_type")
|
||||
grantType := httpapi.ParseCustom(p, vals, "", "grant_type", httpapi.ParseEnum[codersdk.OAuth2ProviderGrantType])
|
||||
|
||||
// Grant-type specific validation - must be called before parsing values.
|
||||
switch grantType {
|
||||
case codersdk.OAuth2ProviderGrantTypeRefreshToken:
|
||||
p.RequiredNotEmpty("refresh_token")
|
||||
@@ -67,19 +55,23 @@ func extractTokenParams(r *http.Request, callbackURL *url.URL) (tokenParams, []c
|
||||
p.RequiredNotEmpty("client_secret", "client_id", "code")
|
||||
}
|
||||
|
||||
params := tokenParams{
|
||||
clientID: p.String(vals, "", "client_id"),
|
||||
clientSecret: p.String(vals, "", "client_secret"),
|
||||
code: p.String(vals, "", "code"),
|
||||
grantType: grantType,
|
||||
redirectURL: p.RedirectURL(vals, callbackURL, "redirect_uri"),
|
||||
refreshToken: p.String(vals, "", "refresh_token"),
|
||||
codeVerifier: p.String(vals, "", "code_verifier"),
|
||||
resource: p.String(vals, "", "resource"),
|
||||
scopes: strings.Fields(strings.TrimSpace(p.String(vals, "", "scope"))),
|
||||
req := codersdk.OAuth2TokenRequest{
|
||||
GrantType: grantType,
|
||||
ClientID: p.String(vals, "", "client_id"),
|
||||
ClientSecret: p.String(vals, "", "client_secret"),
|
||||
Code: p.String(vals, "", "code"),
|
||||
RedirectURI: p.String(vals, "", "redirect_uri"),
|
||||
RefreshToken: p.String(vals, "", "refresh_token"),
|
||||
CodeVerifier: p.String(vals, "", "code_verifier"),
|
||||
Resource: p.String(vals, "", "resource"),
|
||||
Scope: p.String(vals, "", "scope"),
|
||||
}
|
||||
// Validate resource parameter syntax (RFC 8707): must be absolute URI without fragment
|
||||
if err := validateResourceParameter(params.resource); err != nil {
|
||||
|
||||
// Validate redirect URI - errors are added to p.Errors.
|
||||
_ = p.RedirectURL(vals, callbackURL, "redirect_uri")
|
||||
|
||||
// Validate resource parameter syntax (RFC 8707): must be absolute URI without fragment.
|
||||
if err := validateResourceParameter(req.Resource); err != nil {
|
||||
p.Errors = append(p.Errors, codersdk.ValidationError{
|
||||
Field: "resource",
|
||||
Detail: "must be an absolute URI without fragment",
|
||||
@@ -88,9 +80,9 @@ func extractTokenParams(r *http.Request, callbackURL *url.URL) (tokenParams, []c
|
||||
|
||||
p.ErrorExcessParams(vals)
|
||||
if len(p.Errors) > 0 {
|
||||
return tokenParams{}, p.Errors, xerrors.Errorf("invalid query params: %w", p.Errors)
|
||||
return codersdk.OAuth2TokenRequest{}, p.Errors, xerrors.Errorf("invalid query params: %w", p.Errors)
|
||||
}
|
||||
return params, nil, nil
|
||||
return req, nil, nil
|
||||
}
|
||||
|
||||
// Tokens
|
||||
@@ -110,13 +102,13 @@ func Tokens(db database.Store, lifetimes codersdk.SessionLifetime) http.HandlerF
|
||||
return
|
||||
}
|
||||
|
||||
params, validationErrs, err := extractTokenParams(r, callbackURL)
|
||||
req, validationErrs, err := extractTokenRequest(r, callbackURL)
|
||||
if err != nil {
|
||||
// Check for specific validation errors in priority order
|
||||
if slices.ContainsFunc(validationErrs, func(validationError codersdk.ValidationError) bool {
|
||||
return validationError.Field == "grant_type"
|
||||
}) {
|
||||
httpapi.WriteOAuth2Error(ctx, rw, http.StatusBadRequest, "unsupported_grant_type", "The grant type is missing or unsupported")
|
||||
httpapi.WriteOAuth2Error(ctx, rw, http.StatusBadRequest, codersdk.OAuth2ErrorCodeUnsupportedGrantType, "The grant type is missing or unsupported")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -125,47 +117,47 @@ func Tokens(db database.Store, lifetimes codersdk.SessionLifetime) http.HandlerF
|
||||
if slices.ContainsFunc(validationErrs, func(validationError codersdk.ValidationError) bool {
|
||||
return validationError.Field == field
|
||||
}) {
|
||||
httpapi.WriteOAuth2Error(ctx, rw, http.StatusBadRequest, "invalid_request", fmt.Sprintf("Missing required parameter: %s", field))
|
||||
httpapi.WriteOAuth2Error(ctx, rw, http.StatusBadRequest, codersdk.OAuth2ErrorCodeInvalidRequest, fmt.Sprintf("Missing required parameter: %s", field))
|
||||
return
|
||||
}
|
||||
}
|
||||
// Generic invalid request for other validation errors
|
||||
httpapi.WriteOAuth2Error(ctx, rw, http.StatusBadRequest, "invalid_request", "The request is missing required parameters or is otherwise malformed")
|
||||
httpapi.WriteOAuth2Error(ctx, rw, http.StatusBadRequest, codersdk.OAuth2ErrorCodeInvalidRequest, "The request is missing required parameters or is otherwise malformed")
|
||||
return
|
||||
}
|
||||
|
||||
var token oauth2.Token
|
||||
var token codersdk.OAuth2TokenResponse
|
||||
//nolint:gocritic,revive // More cases will be added later.
|
||||
switch params.grantType {
|
||||
switch req.GrantType {
|
||||
// TODO: Client creds, device code.
|
||||
case codersdk.OAuth2ProviderGrantTypeRefreshToken:
|
||||
token, err = refreshTokenGrant(ctx, db, app, lifetimes, params)
|
||||
token, err = refreshTokenGrant(ctx, db, app, lifetimes, req)
|
||||
case codersdk.OAuth2ProviderGrantTypeAuthorizationCode:
|
||||
token, err = authorizationCodeGrant(ctx, db, app, lifetimes, params)
|
||||
token, err = authorizationCodeGrant(ctx, db, app, lifetimes, req)
|
||||
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))
|
||||
httpapi.WriteOAuth2Error(ctx, rw, http.StatusBadRequest, codersdk.OAuth2ErrorCodeUnsupportedGrantType, fmt.Sprintf("The grant type %q is not supported", req.GrantType))
|
||||
return
|
||||
}
|
||||
|
||||
if errors.Is(err, errBadSecret) {
|
||||
httpapi.WriteOAuth2Error(ctx, rw, http.StatusUnauthorized, "invalid_client", "The client credentials are invalid")
|
||||
httpapi.WriteOAuth2Error(ctx, rw, http.StatusUnauthorized, codersdk.OAuth2ErrorCodeInvalidClient, "The client credentials are invalid")
|
||||
return
|
||||
}
|
||||
if errors.Is(err, errBadCode) {
|
||||
httpapi.WriteOAuth2Error(ctx, rw, http.StatusBadRequest, "invalid_grant", "The authorization code is invalid or expired")
|
||||
httpapi.WriteOAuth2Error(ctx, rw, http.StatusBadRequest, codersdk.OAuth2ErrorCodeInvalidGrant, "The authorization code is invalid or expired")
|
||||
return
|
||||
}
|
||||
if errors.Is(err, errInvalidPKCE) {
|
||||
httpapi.WriteOAuth2Error(ctx, rw, http.StatusBadRequest, "invalid_grant", "The PKCE code verifier is invalid")
|
||||
httpapi.WriteOAuth2Error(ctx, rw, http.StatusBadRequest, codersdk.OAuth2ErrorCodeInvalidGrant, "The PKCE code verifier is invalid")
|
||||
return
|
||||
}
|
||||
if errors.Is(err, errInvalidResource) {
|
||||
httpapi.WriteOAuth2Error(ctx, rw, http.StatusBadRequest, "invalid_target", "The resource parameter is invalid")
|
||||
httpapi.WriteOAuth2Error(ctx, rw, http.StatusBadRequest, codersdk.OAuth2ErrorCodeInvalidTarget, "The resource parameter is invalid")
|
||||
return
|
||||
}
|
||||
if errors.Is(err, errBadToken) {
|
||||
httpapi.WriteOAuth2Error(ctx, rw, http.StatusBadRequest, "invalid_grant", "The refresh token is invalid or expired")
|
||||
httpapi.WriteOAuth2Error(ctx, rw, http.StatusBadRequest, codersdk.OAuth2ErrorCodeInvalidGrant, "The refresh token is invalid or expired")
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
@@ -182,77 +174,77 @@ func Tokens(db database.Store, lifetimes codersdk.SessionLifetime) http.HandlerF
|
||||
}
|
||||
}
|
||||
|
||||
func authorizationCodeGrant(ctx context.Context, db database.Store, app database.OAuth2ProviderApp, lifetimes codersdk.SessionLifetime, params tokenParams) (oauth2.Token, error) {
|
||||
func authorizationCodeGrant(ctx context.Context, db database.Store, app database.OAuth2ProviderApp, lifetimes codersdk.SessionLifetime, req codersdk.OAuth2TokenRequest) (codersdk.OAuth2TokenResponse, error) {
|
||||
// Validate the client secret.
|
||||
secret, err := ParseFormattedSecret(params.clientSecret)
|
||||
secret, err := ParseFormattedSecret(req.ClientSecret)
|
||||
if err != nil {
|
||||
return oauth2.Token{}, errBadSecret
|
||||
return codersdk.OAuth2TokenResponse{}, errBadSecret
|
||||
}
|
||||
//nolint:gocritic // Users cannot read secrets so we must use the system.
|
||||
dbSecret, err := db.GetOAuth2ProviderAppSecretByPrefix(dbauthz.AsSystemRestricted(ctx), []byte(secret.Prefix))
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return oauth2.Token{}, errBadSecret
|
||||
return codersdk.OAuth2TokenResponse{}, errBadSecret
|
||||
}
|
||||
if err != nil {
|
||||
return oauth2.Token{}, err
|
||||
return codersdk.OAuth2TokenResponse{}, err
|
||||
}
|
||||
|
||||
equalSecret := apikey.ValidateHash(dbSecret.HashedSecret, secret.Secret)
|
||||
if !equalSecret {
|
||||
return oauth2.Token{}, errBadSecret
|
||||
return codersdk.OAuth2TokenResponse{}, errBadSecret
|
||||
}
|
||||
|
||||
// Validate the authorization code.
|
||||
code, err := ParseFormattedSecret(params.code)
|
||||
code, err := ParseFormattedSecret(req.Code)
|
||||
if err != nil {
|
||||
return oauth2.Token{}, errBadCode
|
||||
return codersdk.OAuth2TokenResponse{}, errBadCode
|
||||
}
|
||||
//nolint:gocritic // There is no user yet so we must use the system.
|
||||
dbCode, err := db.GetOAuth2ProviderAppCodeByPrefix(dbauthz.AsSystemRestricted(ctx), []byte(code.Prefix))
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return oauth2.Token{}, errBadCode
|
||||
return codersdk.OAuth2TokenResponse{}, errBadCode
|
||||
}
|
||||
if err != nil {
|
||||
return oauth2.Token{}, err
|
||||
return codersdk.OAuth2TokenResponse{}, err
|
||||
}
|
||||
equalCode := apikey.ValidateHash(dbCode.HashedSecret, code.Secret)
|
||||
if !equalCode {
|
||||
return oauth2.Token{}, errBadCode
|
||||
return codersdk.OAuth2TokenResponse{}, errBadCode
|
||||
}
|
||||
|
||||
// Ensure the code has not expired.
|
||||
if dbCode.ExpiresAt.Before(dbtime.Now()) {
|
||||
return oauth2.Token{}, errBadCode
|
||||
return codersdk.OAuth2TokenResponse{}, errBadCode
|
||||
}
|
||||
|
||||
// Verify PKCE challenge if present
|
||||
if dbCode.CodeChallenge.Valid && dbCode.CodeChallenge.String != "" {
|
||||
if params.codeVerifier == "" {
|
||||
return oauth2.Token{}, errInvalidPKCE
|
||||
if req.CodeVerifier == "" {
|
||||
return codersdk.OAuth2TokenResponse{}, errInvalidPKCE
|
||||
}
|
||||
if !VerifyPKCE(dbCode.CodeChallenge.String, params.codeVerifier) {
|
||||
return oauth2.Token{}, errInvalidPKCE
|
||||
if !VerifyPKCE(dbCode.CodeChallenge.String, req.CodeVerifier) {
|
||||
return codersdk.OAuth2TokenResponse{}, errInvalidPKCE
|
||||
}
|
||||
}
|
||||
|
||||
// Verify resource parameter consistency (RFC 8707)
|
||||
if dbCode.ResourceUri.Valid && dbCode.ResourceUri.String != "" {
|
||||
// Resource was specified during authorization - it must match in token request
|
||||
if params.resource == "" {
|
||||
return oauth2.Token{}, errInvalidResource
|
||||
if req.Resource == "" {
|
||||
return codersdk.OAuth2TokenResponse{}, errInvalidResource
|
||||
}
|
||||
if params.resource != dbCode.ResourceUri.String {
|
||||
return oauth2.Token{}, errInvalidResource
|
||||
if req.Resource != dbCode.ResourceUri.String {
|
||||
return codersdk.OAuth2TokenResponse{}, errInvalidResource
|
||||
}
|
||||
} else if params.resource != "" {
|
||||
} else if req.Resource != "" {
|
||||
// Resource was not specified during authorization but is now provided
|
||||
return oauth2.Token{}, errInvalidResource
|
||||
return codersdk.OAuth2TokenResponse{}, errInvalidResource
|
||||
}
|
||||
|
||||
// Generate a refresh token.
|
||||
refreshToken, err := GenerateSecret()
|
||||
if err != nil {
|
||||
return oauth2.Token{}, err
|
||||
return codersdk.OAuth2TokenResponse{}, err
|
||||
}
|
||||
|
||||
// Generate the API key we will swap for the code.
|
||||
@@ -266,13 +258,13 @@ func authorizationCodeGrant(ctx context.Context, db database.Store, app database
|
||||
TokenName: tokenName,
|
||||
})
|
||||
if err != nil {
|
||||
return oauth2.Token{}, err
|
||||
return codersdk.OAuth2TokenResponse{}, err
|
||||
}
|
||||
|
||||
// Grab the user roles so we can perform the exchange as the user.
|
||||
actor, _, err := httpmw.UserRBACSubject(ctx, db, dbCode.UserID, rbac.ScopeAll)
|
||||
if err != nil {
|
||||
return oauth2.Token{}, xerrors.Errorf("fetch user actor: %w", err)
|
||||
return codersdk.OAuth2TokenResponse{}, xerrors.Errorf("fetch user actor: %w", err)
|
||||
}
|
||||
|
||||
// Do the actual token exchange in the database.
|
||||
@@ -324,47 +316,47 @@ func authorizationCodeGrant(ctx context.Context, db database.Store, app database
|
||||
return nil
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return oauth2.Token{}, err
|
||||
return codersdk.OAuth2TokenResponse{}, err
|
||||
}
|
||||
|
||||
return oauth2.Token{
|
||||
return codersdk.OAuth2TokenResponse{
|
||||
AccessToken: sessionToken,
|
||||
TokenType: "Bearer",
|
||||
TokenType: codersdk.OAuth2TokenTypeBearer,
|
||||
RefreshToken: refreshToken.Formatted,
|
||||
Expiry: key.ExpiresAt,
|
||||
ExpiresIn: int64(time.Until(key.ExpiresAt).Seconds()),
|
||||
Expiry: &key.ExpiresAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func refreshTokenGrant(ctx context.Context, db database.Store, app database.OAuth2ProviderApp, lifetimes codersdk.SessionLifetime, params tokenParams) (oauth2.Token, error) {
|
||||
func refreshTokenGrant(ctx context.Context, db database.Store, app database.OAuth2ProviderApp, lifetimes codersdk.SessionLifetime, req codersdk.OAuth2TokenRequest) (codersdk.OAuth2TokenResponse, error) {
|
||||
// Validate the token.
|
||||
token, err := ParseFormattedSecret(params.refreshToken)
|
||||
token, err := ParseFormattedSecret(req.RefreshToken)
|
||||
if err != nil {
|
||||
return oauth2.Token{}, errBadToken
|
||||
return codersdk.OAuth2TokenResponse{}, errBadToken
|
||||
}
|
||||
//nolint:gocritic // There is no user yet so we must use the system.
|
||||
dbToken, err := db.GetOAuth2ProviderAppTokenByPrefix(dbauthz.AsSystemRestricted(ctx), []byte(token.Prefix))
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return oauth2.Token{}, errBadToken
|
||||
return codersdk.OAuth2TokenResponse{}, errBadToken
|
||||
}
|
||||
if err != nil {
|
||||
return oauth2.Token{}, err
|
||||
return codersdk.OAuth2TokenResponse{}, err
|
||||
}
|
||||
equal := apikey.ValidateHash(dbToken.RefreshHash, token.Secret)
|
||||
if !equal {
|
||||
return oauth2.Token{}, errBadToken
|
||||
return codersdk.OAuth2TokenResponse{}, errBadToken
|
||||
}
|
||||
|
||||
// Ensure the token has not expired.
|
||||
if dbToken.ExpiresAt.Before(dbtime.Now()) {
|
||||
return oauth2.Token{}, errBadToken
|
||||
return codersdk.OAuth2TokenResponse{}, errBadToken
|
||||
}
|
||||
|
||||
// Verify resource parameter consistency for refresh tokens (RFC 8707)
|
||||
if params.resource != "" {
|
||||
if req.Resource != "" {
|
||||
// If resource is provided in refresh request, it must match the original token's audience
|
||||
if !dbToken.Audience.Valid || dbToken.Audience.String != params.resource {
|
||||
return oauth2.Token{}, errInvalidResource
|
||||
if !dbToken.Audience.Valid || dbToken.Audience.String != req.Resource {
|
||||
return codersdk.OAuth2TokenResponse{}, errInvalidResource
|
||||
}
|
||||
}
|
||||
|
||||
@@ -372,18 +364,18 @@ func refreshTokenGrant(ctx context.Context, db database.Store, app database.OAut
|
||||
//nolint:gocritic // There is no user yet so we must use the system.
|
||||
prevKey, err := db.GetAPIKeyByID(dbauthz.AsSystemRestricted(ctx), dbToken.APIKeyID)
|
||||
if err != nil {
|
||||
return oauth2.Token{}, err
|
||||
return codersdk.OAuth2TokenResponse{}, err
|
||||
}
|
||||
|
||||
actor, _, err := httpmw.UserRBACSubject(ctx, db, prevKey.UserID, rbac.ScopeAll)
|
||||
if err != nil {
|
||||
return oauth2.Token{}, xerrors.Errorf("fetch user actor: %w", err)
|
||||
return codersdk.OAuth2TokenResponse{}, xerrors.Errorf("fetch user actor: %w", err)
|
||||
}
|
||||
|
||||
// Generate a new refresh token.
|
||||
refreshToken, err := GenerateSecret()
|
||||
if err != nil {
|
||||
return oauth2.Token{}, err
|
||||
return codersdk.OAuth2TokenResponse{}, err
|
||||
}
|
||||
|
||||
// Generate the new API key.
|
||||
@@ -397,7 +389,7 @@ func refreshTokenGrant(ctx context.Context, db database.Store, app database.OAut
|
||||
TokenName: tokenName,
|
||||
})
|
||||
if err != nil {
|
||||
return oauth2.Token{}, err
|
||||
return codersdk.OAuth2TokenResponse{}, err
|
||||
}
|
||||
|
||||
// Replace the token.
|
||||
@@ -437,15 +429,15 @@ func refreshTokenGrant(ctx context.Context, db database.Store, app database.OAut
|
||||
return nil
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return oauth2.Token{}, err
|
||||
return codersdk.OAuth2TokenResponse{}, err
|
||||
}
|
||||
|
||||
return oauth2.Token{
|
||||
return codersdk.OAuth2TokenResponse{
|
||||
AccessToken: sessionToken,
|
||||
TokenType: "Bearer",
|
||||
TokenType: codersdk.OAuth2TokenTypeBearer,
|
||||
RefreshToken: refreshToken.Formatted,
|
||||
Expiry: key.ExpiresAt,
|
||||
ExpiresIn: int64(time.Until(key.ExpiresAt).Seconds()),
|
||||
Expiry: &key.ExpiresAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package oauth2provider
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -10,6 +11,12 @@ import (
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
// parseScopes parses a space-delimited scope string into a slice of scopes
|
||||
// per RFC 6749.
|
||||
func parseScopes(scope string) []string {
|
||||
return strings.Fields(strings.TrimSpace(scope))
|
||||
}
|
||||
|
||||
// TestExtractTokenParams_Scopes tests OAuth2 scope parameter parsing
|
||||
// to ensure RFC 6749 compliance where scopes are space-delimited
|
||||
func TestExtractTokenParams_Scopes(t *testing.T) {
|
||||
@@ -115,15 +122,15 @@ func TestExtractTokenParams_Scopes(t *testing.T) {
|
||||
Form: form, // Form is the combination of PostForm and URL query
|
||||
}
|
||||
|
||||
// Extract token params
|
||||
params, validationErrs, err := extractTokenParams(req, callbackURL)
|
||||
// Extract token request
|
||||
tokenReq, validationErrs, err := extractTokenRequest(req, callbackURL)
|
||||
|
||||
// Verify no errors occurred
|
||||
require.NoError(t, err, "extractTokenParams should not return error for: %s", tc.description)
|
||||
require.NoError(t, err, "extractTokenRequest should not return error for: %s", tc.description)
|
||||
require.Empty(t, validationErrs, "should have no validation errors for: %s", tc.description)
|
||||
|
||||
// Verify scopes match expected
|
||||
require.Equal(t, tc.expectedScopes, params.scopes, "scope parsing failed for: %s", tc.description)
|
||||
require.Equal(t, tc.expectedScopes, parseScopes(tokenReq.Scope), "scope parsing failed for: %s", tc.description)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -178,15 +185,15 @@ func TestExtractTokenParams_ScopesURLEncoded(t *testing.T) {
|
||||
Form: values,
|
||||
}
|
||||
|
||||
// Extract token params
|
||||
params, validationErrs, err := extractTokenParams(req, callbackURL)
|
||||
// Extract token request
|
||||
tokenReq, validationErrs, err := extractTokenRequest(req, callbackURL)
|
||||
|
||||
// Verify no errors
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, validationErrs)
|
||||
|
||||
// Verify scopes
|
||||
require.Equal(t, tc.expectedScopes, params.scopes)
|
||||
require.Equal(t, tc.expectedScopes, parseScopes(tokenReq.Scope))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -259,11 +266,11 @@ func TestExtractTokenParams_ScopesEdgeCases(t *testing.T) {
|
||||
Form: form,
|
||||
}
|
||||
|
||||
params, validationErrs, err := extractTokenParams(req, callbackURL)
|
||||
tokenReq, validationErrs, err := extractTokenRequest(req, callbackURL)
|
||||
|
||||
require.NoError(t, err, "extractTokenParams should not error for: %s", tc.description)
|
||||
require.NoError(t, err, "extractTokenRequest should not error for: %s", tc.description)
|
||||
require.Empty(t, validationErrs)
|
||||
require.Equal(t, tc.expectedScopes, params.scopes, "scope mismatch for: %s", tc.description)
|
||||
require.Equal(t, tc.expectedScopes, parseScopes(tokenReq.Scope), "scope mismatch for: %s", tc.description)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -354,10 +361,10 @@ func TestRefreshTokenGrant_Scopes(t *testing.T) {
|
||||
Form: form,
|
||||
}
|
||||
|
||||
params, validationErrs, err := extractTokenParams(req, callbackURL)
|
||||
tokenReq, validationErrs, err := extractTokenRequest(req, callbackURL)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, validationErrs)
|
||||
require.Equal(t, codersdk.OAuth2ProviderGrantTypeRefreshToken, params.grantType)
|
||||
require.Equal(t, []string{"reduced:scope", "subset:scope"}, params.scopes)
|
||||
require.Equal(t, codersdk.OAuth2ProviderGrantTypeRefreshToken, tokenReq.GrantType)
|
||||
require.Equal(t, []string{"reduced:scope", "subset:scope"}, parseScopes(tokenReq.Scope))
|
||||
}
|
||||
|
||||
@@ -277,47 +277,47 @@ func TestOAuth2ClientMetadataValidation(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
grantTypes []string
|
||||
grantTypes []codersdk.OAuth2ProviderGrantType
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "DefaultEmpty",
|
||||
grantTypes: []string{},
|
||||
grantTypes: []codersdk.OAuth2ProviderGrantType{},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "ValidAuthorizationCode",
|
||||
grantTypes: []string{"authorization_code"},
|
||||
grantTypes: []codersdk.OAuth2ProviderGrantType{codersdk.OAuth2ProviderGrantTypeAuthorizationCode},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "InvalidRefreshTokenAlone",
|
||||
grantTypes: []string{"refresh_token"},
|
||||
grantTypes: []codersdk.OAuth2ProviderGrantType{codersdk.OAuth2ProviderGrantTypeRefreshToken},
|
||||
expectError: true, // refresh_token requires authorization_code to be present
|
||||
},
|
||||
{
|
||||
name: "ValidMultiple",
|
||||
grantTypes: []string{"authorization_code", "refresh_token"},
|
||||
grantTypes: []codersdk.OAuth2ProviderGrantType{codersdk.OAuth2ProviderGrantTypeAuthorizationCode, codersdk.OAuth2ProviderGrantTypeRefreshToken},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "InvalidUnsupported",
|
||||
grantTypes: []string{"client_credentials"},
|
||||
grantTypes: []codersdk.OAuth2ProviderGrantType{codersdk.OAuth2ProviderGrantTypeClientCredentials},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "InvalidPassword",
|
||||
grantTypes: []string{"password"},
|
||||
grantTypes: []codersdk.OAuth2ProviderGrantType{codersdk.OAuth2ProviderGrantTypePassword},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "InvalidImplicit",
|
||||
grantTypes: []string{"implicit"},
|
||||
grantTypes: []codersdk.OAuth2ProviderGrantType{codersdk.OAuth2ProviderGrantTypeImplicit},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "MixedValidInvalid",
|
||||
grantTypes: []string{"authorization_code", "client_credentials"},
|
||||
grantTypes: []codersdk.OAuth2ProviderGrantType{codersdk.OAuth2ProviderGrantTypeAuthorizationCode, codersdk.OAuth2ProviderGrantTypeClientCredentials},
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
@@ -352,32 +352,32 @@ func TestOAuth2ClientMetadataValidation(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
responseTypes []string
|
||||
responseTypes []codersdk.OAuth2ProviderResponseType
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "DefaultEmpty",
|
||||
responseTypes: []string{},
|
||||
responseTypes: []codersdk.OAuth2ProviderResponseType{},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "ValidCode",
|
||||
responseTypes: []string{"code"},
|
||||
responseTypes: []codersdk.OAuth2ProviderResponseType{codersdk.OAuth2ProviderResponseTypeCode},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "InvalidToken",
|
||||
responseTypes: []string{"token"},
|
||||
responseTypes: []codersdk.OAuth2ProviderResponseType{codersdk.OAuth2ProviderResponseTypeToken},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "InvalidImplicit",
|
||||
responseTypes: []string{"id_token"},
|
||||
name: "InvalidIDToken",
|
||||
responseTypes: []codersdk.OAuth2ProviderResponseType{"id_token"}, // OIDC-specific, no constant
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "InvalidMultiple",
|
||||
responseTypes: []string{"code", "token"},
|
||||
responseTypes: []codersdk.OAuth2ProviderResponseType{codersdk.OAuth2ProviderResponseTypeCode, codersdk.OAuth2ProviderResponseTypeToken},
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
@@ -412,7 +412,7 @@ func TestOAuth2ClientMetadataValidation(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
authMethod string
|
||||
authMethod codersdk.OAuth2TokenEndpointAuthMethod
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
@@ -422,27 +422,27 @@ func TestOAuth2ClientMetadataValidation(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "ValidClientSecretBasic",
|
||||
authMethod: "client_secret_basic",
|
||||
authMethod: codersdk.OAuth2TokenEndpointAuthMethodClientSecretBasic,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "ValidClientSecretPost",
|
||||
authMethod: "client_secret_post",
|
||||
authMethod: codersdk.OAuth2TokenEndpointAuthMethodClientSecretPost,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "ValidNone",
|
||||
authMethod: "none",
|
||||
authMethod: codersdk.OAuth2TokenEndpointAuthMethodNone,
|
||||
expectError: false, // "none" is valid for public clients per RFC 7591
|
||||
},
|
||||
{
|
||||
name: "InvalidPrivateKeyJWT",
|
||||
authMethod: "private_key_jwt",
|
||||
authMethod: "private_key_jwt", // OIDC-specific, no constant defined
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "InvalidClientSecretJWT",
|
||||
authMethod: "client_secret_jwt",
|
||||
authMethod: "client_secret_jwt", // OIDC-specific, no constant defined
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
@@ -659,14 +659,14 @@ func TestOAuth2ClientMetadataDefaults(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should default to authorization_code
|
||||
require.Contains(t, config.GrantTypes, "authorization_code")
|
||||
require.Contains(t, config.GrantTypes, codersdk.OAuth2ProviderGrantTypeAuthorizationCode)
|
||||
|
||||
// Should default to code
|
||||
require.Contains(t, config.ResponseTypes, "code")
|
||||
require.Contains(t, config.ResponseTypes, codersdk.OAuth2ProviderResponseTypeCode)
|
||||
|
||||
// Should default to client_secret_basic or client_secret_post
|
||||
require.True(t, config.TokenEndpointAuthMethod == "client_secret_basic" ||
|
||||
config.TokenEndpointAuthMethod == "client_secret_post" ||
|
||||
require.True(t, config.TokenEndpointAuthMethod == codersdk.OAuth2TokenEndpointAuthMethodClientSecretBasic ||
|
||||
config.TokenEndpointAuthMethod == codersdk.OAuth2TokenEndpointAuthMethodClientSecretPost ||
|
||||
config.TokenEndpointAuthMethod == "")
|
||||
|
||||
// Client secret should be generated
|
||||
|
||||
+248
-88
@@ -8,6 +8,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
@@ -186,14 +187,22 @@ func (c *Client) DeleteOAuth2ProviderAppSecret(ctx context.Context, appID uuid.U
|
||||
|
||||
type OAuth2ProviderGrantType string
|
||||
|
||||
// OAuth2ProviderGrantType values (RFC 6749).
|
||||
const (
|
||||
OAuth2ProviderGrantTypeAuthorizationCode OAuth2ProviderGrantType = "authorization_code"
|
||||
OAuth2ProviderGrantTypeRefreshToken OAuth2ProviderGrantType = "refresh_token"
|
||||
OAuth2ProviderGrantTypePassword OAuth2ProviderGrantType = "password"
|
||||
OAuth2ProviderGrantTypeClientCredentials OAuth2ProviderGrantType = "client_credentials"
|
||||
OAuth2ProviderGrantTypeImplicit OAuth2ProviderGrantType = "implicit"
|
||||
)
|
||||
|
||||
func (e OAuth2ProviderGrantType) Valid() bool {
|
||||
switch e {
|
||||
case OAuth2ProviderGrantTypeAuthorizationCode, OAuth2ProviderGrantTypeRefreshToken:
|
||||
case OAuth2ProviderGrantTypeAuthorizationCode,
|
||||
OAuth2ProviderGrantTypeRefreshToken,
|
||||
OAuth2ProviderGrantTypePassword,
|
||||
OAuth2ProviderGrantTypeClientCredentials,
|
||||
OAuth2ProviderGrantTypeImplicit:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -201,19 +210,171 @@ func (e OAuth2ProviderGrantType) Valid() bool {
|
||||
|
||||
type OAuth2ProviderResponseType string
|
||||
|
||||
// OAuth2ProviderResponseType values (RFC 6749).
|
||||
const (
|
||||
OAuth2ProviderResponseTypeCode OAuth2ProviderResponseType = "code"
|
||||
OAuth2ProviderResponseTypeCode OAuth2ProviderResponseType = "code"
|
||||
OAuth2ProviderResponseTypeToken OAuth2ProviderResponseType = "token"
|
||||
)
|
||||
|
||||
func (e OAuth2ProviderResponseType) Valid() bool {
|
||||
//nolint:gocritic,revive // More cases might be added later.
|
||||
switch e {
|
||||
case OAuth2ProviderResponseTypeCode:
|
||||
case OAuth2ProviderResponseTypeCode, OAuth2ProviderResponseTypeToken:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type OAuth2TokenEndpointAuthMethod string
|
||||
|
||||
const (
|
||||
OAuth2TokenEndpointAuthMethodClientSecretBasic OAuth2TokenEndpointAuthMethod = "client_secret_basic"
|
||||
OAuth2TokenEndpointAuthMethodClientSecretPost OAuth2TokenEndpointAuthMethod = "client_secret_post"
|
||||
OAuth2TokenEndpointAuthMethodNone OAuth2TokenEndpointAuthMethod = "none"
|
||||
)
|
||||
|
||||
func (m OAuth2TokenEndpointAuthMethod) Valid() bool {
|
||||
switch m {
|
||||
case OAuth2TokenEndpointAuthMethodClientSecretBasic,
|
||||
OAuth2TokenEndpointAuthMethodClientSecretPost,
|
||||
OAuth2TokenEndpointAuthMethodNone:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type OAuth2PKCECodeChallengeMethod string
|
||||
|
||||
// OAuth2PKCECodeChallengeMethod values (RFC 7636).
|
||||
const (
|
||||
OAuth2PKCECodeChallengeMethodS256 OAuth2PKCECodeChallengeMethod = "S256"
|
||||
OAuth2PKCECodeChallengeMethodPlain OAuth2PKCECodeChallengeMethod = "plain"
|
||||
)
|
||||
|
||||
func (m OAuth2PKCECodeChallengeMethod) Valid() bool {
|
||||
switch m {
|
||||
case OAuth2PKCECodeChallengeMethodS256, OAuth2PKCECodeChallengeMethodPlain:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type OAuth2TokenType string
|
||||
|
||||
// OAuth2TokenType values (RFC 6749, RFC 9449).
|
||||
const (
|
||||
OAuth2TokenTypeBearer OAuth2TokenType = "Bearer"
|
||||
OAuth2TokenTypeDPoP OAuth2TokenType = "DPoP"
|
||||
)
|
||||
|
||||
func (t OAuth2TokenType) Valid() bool {
|
||||
switch t {
|
||||
case OAuth2TokenTypeBearer, OAuth2TokenTypeDPoP:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type OAuth2RevocationTokenTypeHint string
|
||||
|
||||
const (
|
||||
OAuth2RevocationTokenTypeHintAccessToken OAuth2RevocationTokenTypeHint = "access_token"
|
||||
OAuth2RevocationTokenTypeHintRefreshToken OAuth2RevocationTokenTypeHint = "refresh_token"
|
||||
)
|
||||
|
||||
func (h OAuth2RevocationTokenTypeHint) Valid() bool {
|
||||
switch h {
|
||||
case OAuth2RevocationTokenTypeHintAccessToken, OAuth2RevocationTokenTypeHintRefreshToken:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type OAuth2ErrorCode string
|
||||
|
||||
// OAuth2 error codes per RFC 6749, RFC 7009, RFC 8707.
|
||||
// This is not comprehensive; it includes only codes relevant to this implementation.
|
||||
const (
|
||||
// RFC 6749 - Token endpoint errors.
|
||||
OAuth2ErrorCodeInvalidRequest OAuth2ErrorCode = "invalid_request"
|
||||
OAuth2ErrorCodeInvalidClient OAuth2ErrorCode = "invalid_client"
|
||||
OAuth2ErrorCodeInvalidGrant OAuth2ErrorCode = "invalid_grant"
|
||||
OAuth2ErrorCodeUnauthorizedClient OAuth2ErrorCode = "unauthorized_client"
|
||||
OAuth2ErrorCodeUnsupportedGrantType OAuth2ErrorCode = "unsupported_grant_type"
|
||||
OAuth2ErrorCodeInvalidScope OAuth2ErrorCode = "invalid_scope"
|
||||
|
||||
// RFC 6749 - Authorization endpoint errors.
|
||||
OAuth2ErrorCodeAccessDenied OAuth2ErrorCode = "access_denied"
|
||||
OAuth2ErrorCodeUnsupportedResponseType OAuth2ErrorCode = "unsupported_response_type"
|
||||
OAuth2ErrorCodeServerError OAuth2ErrorCode = "server_error"
|
||||
OAuth2ErrorCodeTemporarilyUnavailable OAuth2ErrorCode = "temporarily_unavailable"
|
||||
|
||||
// RFC 7009 - Token revocation errors.
|
||||
OAuth2ErrorCodeUnsupportedTokenType OAuth2ErrorCode = "unsupported_token_type"
|
||||
|
||||
// RFC 8707 - Resource indicator errors.
|
||||
OAuth2ErrorCodeInvalidTarget OAuth2ErrorCode = "invalid_target"
|
||||
)
|
||||
|
||||
func (c OAuth2ErrorCode) Valid() bool {
|
||||
switch c {
|
||||
case OAuth2ErrorCodeInvalidRequest,
|
||||
OAuth2ErrorCodeInvalidClient,
|
||||
OAuth2ErrorCodeInvalidGrant,
|
||||
OAuth2ErrorCodeUnauthorizedClient,
|
||||
OAuth2ErrorCodeUnsupportedGrantType,
|
||||
OAuth2ErrorCodeInvalidScope,
|
||||
OAuth2ErrorCodeAccessDenied,
|
||||
OAuth2ErrorCodeUnsupportedResponseType,
|
||||
OAuth2ErrorCodeServerError,
|
||||
OAuth2ErrorCodeTemporarilyUnavailable,
|
||||
OAuth2ErrorCodeUnsupportedTokenType,
|
||||
OAuth2ErrorCodeInvalidTarget:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// OAuth2Error represents an OAuth2-compliant error response per RFC 6749.
|
||||
type OAuth2Error struct {
|
||||
Error OAuth2ErrorCode `json:"error"`
|
||||
ErrorDescription string `json:"error_description,omitempty"`
|
||||
ErrorURI string `json:"error_uri,omitempty"`
|
||||
}
|
||||
|
||||
// OAuth2TokenRequest represents a token request per RFC 6749. The actual wire
|
||||
// format is application/x-www-form-urlencoded; this struct is for SDK docs.
|
||||
type OAuth2TokenRequest struct {
|
||||
GrantType OAuth2ProviderGrantType `json:"grant_type"`
|
||||
Code string `json:"code,omitempty"`
|
||||
RedirectURI string `json:"redirect_uri,omitempty"`
|
||||
ClientID string `json:"client_id,omitempty"`
|
||||
ClientSecret string `json:"client_secret,omitempty"`
|
||||
CodeVerifier string `json:"code_verifier,omitempty"`
|
||||
RefreshToken string `json:"refresh_token,omitempty"`
|
||||
Resource string `json:"resource,omitempty"`
|
||||
Scope string `json:"scope,omitempty"`
|
||||
}
|
||||
|
||||
// OAuth2TokenResponse represents a successful token response per RFC 6749.
|
||||
type OAuth2TokenResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
TokenType OAuth2TokenType `json:"token_type"`
|
||||
ExpiresIn int64 `json:"expires_in,omitempty"`
|
||||
RefreshToken string `json:"refresh_token,omitempty"`
|
||||
Scope string `json:"scope,omitempty"`
|
||||
// Expiry is not part of RFC 6749 but is included for compatibility with
|
||||
// golang.org/x/oauth2.Token and clients that expect a timestamp.
|
||||
Expiry *time.Time `json:"expiry,omitempty" format:"date-time"`
|
||||
}
|
||||
|
||||
// OAuth2TokenRevocationRequest represents a token revocation request per RFC 7009.
|
||||
type OAuth2TokenRevocationRequest struct {
|
||||
Token string `json:"token"`
|
||||
TokenTypeHint OAuth2RevocationTokenTypeHint `json:"token_type_hint,omitempty"`
|
||||
ClientID string `json:"client_id,omitempty"`
|
||||
ClientSecret string `json:"client_secret,omitempty"`
|
||||
}
|
||||
|
||||
// RevokeOAuth2Token revokes a specific OAuth2 token using RFC 7009 token revocation.
|
||||
func (c *Client) RevokeOAuth2Token(ctx context.Context, clientID uuid.UUID, token string) error {
|
||||
form := url.Values{}
|
||||
@@ -256,18 +417,18 @@ type OAuth2DeviceFlowCallbackResponse struct {
|
||||
RedirectURL string `json:"redirect_url"`
|
||||
}
|
||||
|
||||
// OAuth2AuthorizationServerMetadata represents RFC 8414 OAuth 2.0 Authorization Server Metadata
|
||||
// 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"`
|
||||
RegistrationEndpoint string `json:"registration_endpoint,omitempty"`
|
||||
RevocationEndpoint string `json:"revocation_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"`
|
||||
RegistrationEndpoint string `json:"registration_endpoint,omitempty"`
|
||||
RevocationEndpoint string `json:"revocation_endpoint,omitempty"`
|
||||
ResponseTypesSupported []OAuth2ProviderResponseType `json:"response_types_supported"`
|
||||
GrantTypesSupported []OAuth2ProviderGrantType `json:"grant_types_supported,omitempty"`
|
||||
CodeChallengeMethodsSupported []OAuth2PKCECodeChallengeMethod `json:"code_challenge_methods_supported,omitempty"`
|
||||
ScopesSupported []string `json:"scopes_supported,omitempty"`
|
||||
TokenEndpointAuthMethodsSupported []OAuth2TokenEndpointAuthMethod `json:"token_endpoint_auth_methods_supported,omitempty"`
|
||||
}
|
||||
|
||||
// OAuth2ProtectedResourceMetadata represents RFC 9728 OAuth 2.0 Protected Resource Metadata
|
||||
@@ -278,50 +439,50 @@ type OAuth2ProtectedResourceMetadata struct {
|
||||
BearerMethodsSupported []string `json:"bearer_methods_supported,omitempty"`
|
||||
}
|
||||
|
||||
// OAuth2ClientRegistrationRequest represents RFC 7591 Dynamic Client Registration Request
|
||||
// OAuth2ClientRegistrationRequest represents RFC 7591 Dynamic Client Registration Request.
|
||||
type OAuth2ClientRegistrationRequest struct {
|
||||
RedirectURIs []string `json:"redirect_uris,omitempty"`
|
||||
ClientName string `json:"client_name,omitempty"`
|
||||
ClientURI string `json:"client_uri,omitempty"`
|
||||
LogoURI string `json:"logo_uri,omitempty"`
|
||||
TOSURI string `json:"tos_uri,omitempty"`
|
||||
PolicyURI string `json:"policy_uri,omitempty"`
|
||||
JWKSURI string `json:"jwks_uri,omitempty"`
|
||||
JWKS json.RawMessage `json:"jwks,omitempty" swaggertype:"object"`
|
||||
SoftwareID string `json:"software_id,omitempty"`
|
||||
SoftwareVersion string `json:"software_version,omitempty"`
|
||||
SoftwareStatement string `json:"software_statement,omitempty"`
|
||||
GrantTypes []string `json:"grant_types,omitempty"`
|
||||
ResponseTypes []string `json:"response_types,omitempty"`
|
||||
TokenEndpointAuthMethod string `json:"token_endpoint_auth_method,omitempty"`
|
||||
Scope string `json:"scope,omitempty"`
|
||||
Contacts []string `json:"contacts,omitempty"`
|
||||
RedirectURIs []string `json:"redirect_uris,omitempty"`
|
||||
ClientName string `json:"client_name,omitempty"`
|
||||
ClientURI string `json:"client_uri,omitempty"`
|
||||
LogoURI string `json:"logo_uri,omitempty"`
|
||||
TOSURI string `json:"tos_uri,omitempty"`
|
||||
PolicyURI string `json:"policy_uri,omitempty"`
|
||||
JWKSURI string `json:"jwks_uri,omitempty"`
|
||||
JWKS json.RawMessage `json:"jwks,omitempty" swaggertype:"object"`
|
||||
SoftwareID string `json:"software_id,omitempty"`
|
||||
SoftwareVersion string `json:"software_version,omitempty"`
|
||||
SoftwareStatement string `json:"software_statement,omitempty"`
|
||||
GrantTypes []OAuth2ProviderGrantType `json:"grant_types,omitempty"`
|
||||
ResponseTypes []OAuth2ProviderResponseType `json:"response_types,omitempty"`
|
||||
TokenEndpointAuthMethod OAuth2TokenEndpointAuthMethod `json:"token_endpoint_auth_method,omitempty"`
|
||||
Scope string `json:"scope,omitempty"`
|
||||
Contacts []string `json:"contacts,omitempty"`
|
||||
}
|
||||
|
||||
func (req OAuth2ClientRegistrationRequest) ApplyDefaults() OAuth2ClientRegistrationRequest {
|
||||
// Apply grant type defaults
|
||||
// Apply grant type defaults.
|
||||
if len(req.GrantTypes) == 0 {
|
||||
req.GrantTypes = []string{
|
||||
string(OAuth2ProviderGrantTypeAuthorizationCode),
|
||||
string(OAuth2ProviderGrantTypeRefreshToken),
|
||||
req.GrantTypes = []OAuth2ProviderGrantType{
|
||||
OAuth2ProviderGrantTypeAuthorizationCode,
|
||||
OAuth2ProviderGrantTypeRefreshToken,
|
||||
}
|
||||
}
|
||||
|
||||
// Apply response type defaults
|
||||
// Apply response type defaults.
|
||||
if len(req.ResponseTypes) == 0 {
|
||||
req.ResponseTypes = []string{
|
||||
string(OAuth2ProviderResponseTypeCode),
|
||||
req.ResponseTypes = []OAuth2ProviderResponseType{
|
||||
OAuth2ProviderResponseTypeCode,
|
||||
}
|
||||
}
|
||||
|
||||
// Apply token endpoint auth method default (RFC 7591 section 2)
|
||||
// Apply token endpoint auth method default (RFC 7591 section 2).
|
||||
if req.TokenEndpointAuthMethod == "" {
|
||||
// Default according to RFC 7591: "client_secret_basic" for confidential clients
|
||||
// For public clients, should be explicitly set to "none"
|
||||
req.TokenEndpointAuthMethod = "client_secret_basic"
|
||||
// Default according to RFC 7591: "client_secret_basic" for confidential clients.
|
||||
// For public clients, should be explicitly set to "none".
|
||||
req.TokenEndpointAuthMethod = OAuth2TokenEndpointAuthMethodClientSecretBasic
|
||||
}
|
||||
|
||||
// Apply client name default if not provided
|
||||
// Apply client name default if not provided.
|
||||
if req.ClientName == "" {
|
||||
req.ClientName = "Dynamically Registered Client"
|
||||
}
|
||||
@@ -377,29 +538,29 @@ func (req *OAuth2ClientRegistrationRequest) GenerateClientName() string {
|
||||
return "Dynamically Registered Client"
|
||||
}
|
||||
|
||||
// OAuth2ClientRegistrationResponse represents RFC 7591 Dynamic Client Registration Response
|
||||
// OAuth2ClientRegistrationResponse represents RFC 7591 Dynamic Client Registration Response.
|
||||
type OAuth2ClientRegistrationResponse struct {
|
||||
ClientID string `json:"client_id"`
|
||||
ClientSecret string `json:"client_secret,omitempty"`
|
||||
ClientIDIssuedAt int64 `json:"client_id_issued_at"`
|
||||
ClientSecretExpiresAt int64 `json:"client_secret_expires_at,omitempty"`
|
||||
RedirectURIs []string `json:"redirect_uris,omitempty"`
|
||||
ClientName string `json:"client_name,omitempty"`
|
||||
ClientURI string `json:"client_uri,omitempty"`
|
||||
LogoURI string `json:"logo_uri,omitempty"`
|
||||
TOSURI string `json:"tos_uri,omitempty"`
|
||||
PolicyURI string `json:"policy_uri,omitempty"`
|
||||
JWKSURI string `json:"jwks_uri,omitempty"`
|
||||
JWKS json.RawMessage `json:"jwks,omitempty" swaggertype:"object"`
|
||||
SoftwareID string `json:"software_id,omitempty"`
|
||||
SoftwareVersion string `json:"software_version,omitempty"`
|
||||
GrantTypes []string `json:"grant_types"`
|
||||
ResponseTypes []string `json:"response_types"`
|
||||
TokenEndpointAuthMethod string `json:"token_endpoint_auth_method"`
|
||||
Scope string `json:"scope,omitempty"`
|
||||
Contacts []string `json:"contacts,omitempty"`
|
||||
RegistrationAccessToken string `json:"registration_access_token"`
|
||||
RegistrationClientURI string `json:"registration_client_uri"`
|
||||
ClientID string `json:"client_id"`
|
||||
ClientSecret string `json:"client_secret,omitempty"`
|
||||
ClientIDIssuedAt int64 `json:"client_id_issued_at,omitempty"`
|
||||
ClientSecretExpiresAt int64 `json:"client_secret_expires_at,omitempty"`
|
||||
RedirectURIs []string `json:"redirect_uris,omitempty"`
|
||||
ClientName string `json:"client_name,omitempty"`
|
||||
ClientURI string `json:"client_uri,omitempty"`
|
||||
LogoURI string `json:"logo_uri,omitempty"`
|
||||
TOSURI string `json:"tos_uri,omitempty"`
|
||||
PolicyURI string `json:"policy_uri,omitempty"`
|
||||
JWKSURI string `json:"jwks_uri,omitempty"`
|
||||
JWKS json.RawMessage `json:"jwks,omitempty" swaggertype:"object"`
|
||||
SoftwareID string `json:"software_id,omitempty"`
|
||||
SoftwareVersion string `json:"software_version,omitempty"`
|
||||
GrantTypes []OAuth2ProviderGrantType `json:"grant_types"`
|
||||
ResponseTypes []OAuth2ProviderResponseType `json:"response_types"`
|
||||
TokenEndpointAuthMethod OAuth2TokenEndpointAuthMethod `json:"token_endpoint_auth_method"`
|
||||
Scope string `json:"scope,omitempty"`
|
||||
Contacts []string `json:"contacts,omitempty"`
|
||||
RegistrationAccessToken string `json:"registration_access_token"`
|
||||
RegistrationClientURI string `json:"registration_client_uri"`
|
||||
}
|
||||
|
||||
// PostOAuth2ClientRegistration dynamically registers a new OAuth2 client (RFC 7591)
|
||||
@@ -466,27 +627,26 @@ func (c *Client) DeleteOAuth2ClientConfiguration(ctx context.Context, clientID s
|
||||
return nil
|
||||
}
|
||||
|
||||
// OAuth2ClientConfiguration represents RFC 7592 Client Configuration (for GET/PUT operations)
|
||||
// Same as OAuth2ClientRegistrationResponse but without client_secret in GET responses
|
||||
// OAuth2ClientConfiguration represents RFC 7592 Client Read Response.
|
||||
type OAuth2ClientConfiguration struct {
|
||||
ClientID string `json:"client_id"`
|
||||
ClientIDIssuedAt int64 `json:"client_id_issued_at"`
|
||||
ClientSecretExpiresAt int64 `json:"client_secret_expires_at,omitempty"`
|
||||
RedirectURIs []string `json:"redirect_uris,omitempty"`
|
||||
ClientName string `json:"client_name,omitempty"`
|
||||
ClientURI string `json:"client_uri,omitempty"`
|
||||
LogoURI string `json:"logo_uri,omitempty"`
|
||||
TOSURI string `json:"tos_uri,omitempty"`
|
||||
PolicyURI string `json:"policy_uri,omitempty"`
|
||||
JWKSURI string `json:"jwks_uri,omitempty"`
|
||||
JWKS json.RawMessage `json:"jwks,omitempty" swaggertype:"object"`
|
||||
SoftwareID string `json:"software_id,omitempty"`
|
||||
SoftwareVersion string `json:"software_version,omitempty"`
|
||||
GrantTypes []string `json:"grant_types"`
|
||||
ResponseTypes []string `json:"response_types"`
|
||||
TokenEndpointAuthMethod string `json:"token_endpoint_auth_method"`
|
||||
Scope string `json:"scope,omitempty"`
|
||||
Contacts []string `json:"contacts,omitempty"`
|
||||
RegistrationAccessToken []byte `json:"registration_access_token"`
|
||||
RegistrationClientURI string `json:"registration_client_uri"`
|
||||
ClientID string `json:"client_id"`
|
||||
ClientIDIssuedAt int64 `json:"client_id_issued_at"`
|
||||
ClientSecretExpiresAt int64 `json:"client_secret_expires_at,omitempty"`
|
||||
RedirectURIs []string `json:"redirect_uris,omitempty"`
|
||||
ClientName string `json:"client_name,omitempty"`
|
||||
ClientURI string `json:"client_uri,omitempty"`
|
||||
LogoURI string `json:"logo_uri,omitempty"`
|
||||
TOSURI string `json:"tos_uri,omitempty"`
|
||||
PolicyURI string `json:"policy_uri,omitempty"`
|
||||
JWKSURI string `json:"jwks_uri,omitempty"`
|
||||
JWKS json.RawMessage `json:"jwks,omitempty" swaggertype:"object"`
|
||||
SoftwareID string `json:"software_id,omitempty"`
|
||||
SoftwareVersion string `json:"software_version,omitempty"`
|
||||
GrantTypes []OAuth2ProviderGrantType `json:"grant_types"`
|
||||
ResponseTypes []OAuth2ProviderResponseType `json:"response_types"`
|
||||
TokenEndpointAuthMethod OAuth2TokenEndpointAuthMethod `json:"token_endpoint_auth_method"`
|
||||
Scope string `json:"scope,omitempty"`
|
||||
Contacts []string `json:"contacts,omitempty"`
|
||||
RegistrationAccessToken string `json:"registration_access_token,omitempty"`
|
||||
RegistrationClientURI string `json:"registration_client_uri"`
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ func (req *OAuth2ClientRegistrationRequest) Validate() error {
|
||||
}
|
||||
|
||||
// validateRedirectURIs validates redirect URIs according to RFC 7591, 8252
|
||||
func validateRedirectURIs(uris []string, tokenEndpointAuthMethod string) error {
|
||||
func validateRedirectURIs(uris []string, tokenEndpointAuthMethod OAuth2TokenEndpointAuthMethod) error {
|
||||
if len(uris) == 0 {
|
||||
return xerrors.New("at least one redirect URI is required")
|
||||
}
|
||||
@@ -115,7 +115,7 @@ func validateRedirectURIs(uris []string, tokenEndpointAuthMethod string) error {
|
||||
}
|
||||
|
||||
// Determine if this is a public client based on token endpoint auth method
|
||||
isPublicClient := tokenEndpointAuthMethod == "none"
|
||||
isPublicClient := tokenEndpointAuthMethod == OAuth2TokenEndpointAuthMethodNone
|
||||
|
||||
// Handle different validation for public vs confidential clients
|
||||
if uri.Scheme == "http" || uri.Scheme == "https" {
|
||||
@@ -155,23 +155,15 @@ func validateRedirectURIs(uris []string, tokenEndpointAuthMethod string) error {
|
||||
}
|
||||
|
||||
// validateGrantTypes validates OAuth2 grant types
|
||||
func validateGrantTypes(grantTypes []string) error {
|
||||
validGrants := []string{
|
||||
string(OAuth2ProviderGrantTypeAuthorizationCode),
|
||||
string(OAuth2ProviderGrantTypeRefreshToken),
|
||||
// Add more grant types as they are implemented
|
||||
// "client_credentials",
|
||||
// "urn:ietf:params:oauth:grant-type:device_code",
|
||||
}
|
||||
|
||||
func validateGrantTypes(grantTypes []OAuth2ProviderGrantType) error {
|
||||
for _, grant := range grantTypes {
|
||||
if !slices.Contains(validGrants, grant) {
|
||||
if !isSupportedGrantType(grant) {
|
||||
return xerrors.Errorf("unsupported grant type: %s", grant)
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure authorization_code is present if redirect_uris are specified
|
||||
hasAuthCode := slices.Contains(grantTypes, string(OAuth2ProviderGrantTypeAuthorizationCode))
|
||||
hasAuthCode := slices.Contains(grantTypes, OAuth2ProviderGrantTypeAuthorizationCode)
|
||||
if !hasAuthCode {
|
||||
return xerrors.New("authorization_code grant type is required when redirect_uris are specified")
|
||||
}
|
||||
@@ -179,15 +171,18 @@ func validateGrantTypes(grantTypes []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateResponseTypes validates OAuth2 response types
|
||||
func validateResponseTypes(responseTypes []string) error {
|
||||
validResponses := []string{
|
||||
string(OAuth2ProviderResponseTypeCode),
|
||||
// Add more response types as they are implemented
|
||||
func isSupportedGrantType(grant OAuth2ProviderGrantType) bool {
|
||||
switch grant {
|
||||
case OAuth2ProviderGrantTypeAuthorizationCode, OAuth2ProviderGrantTypeRefreshToken:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// validateResponseTypes validates OAuth2 response types
|
||||
func validateResponseTypes(responseTypes []OAuth2ProviderResponseType) error {
|
||||
for _, responseType := range responseTypes {
|
||||
if !slices.Contains(validResponses, responseType) {
|
||||
if !isSupportedResponseType(responseType) {
|
||||
return xerrors.Errorf("unsupported response type: %s", responseType)
|
||||
}
|
||||
}
|
||||
@@ -195,19 +190,34 @@ func validateResponseTypes(responseTypes []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func isSupportedResponseType(responseType OAuth2ProviderResponseType) bool {
|
||||
return responseType == OAuth2ProviderResponseTypeCode
|
||||
}
|
||||
|
||||
// validateTokenEndpointAuthMethod validates token endpoint authentication method
|
||||
func validateTokenEndpointAuthMethod(method string) error {
|
||||
validMethods := []string{
|
||||
"client_secret_post",
|
||||
"client_secret_basic",
|
||||
"none", // for public clients (RFC 7591)
|
||||
// Add more methods as they are implemented
|
||||
// "private_key_jwt",
|
||||
// "client_secret_jwt",
|
||||
func validateTokenEndpointAuthMethod(method OAuth2TokenEndpointAuthMethod) error {
|
||||
if !method.Valid() {
|
||||
return xerrors.Errorf("unsupported token endpoint auth method: %s", method)
|
||||
}
|
||||
|
||||
if !slices.Contains(validMethods, method) {
|
||||
return xerrors.Errorf("unsupported token endpoint auth method: %s", method)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidatePKCECodeChallengeMethod validates PKCE code_challenge_method parameter.
|
||||
// Per OAuth 2.1, only S256 is supported; plain is rejected for security reasons.
|
||||
func ValidatePKCECodeChallengeMethod(method string) error {
|
||||
if method == "" {
|
||||
return nil // Optional, defaults to S256 if code_challenge is provided
|
||||
}
|
||||
|
||||
m := OAuth2PKCECodeChallengeMethod(method)
|
||||
|
||||
if m == OAuth2PKCECodeChallengeMethodPlain {
|
||||
return xerrors.New("code_challenge_method 'plain' is not supported; use 'S256'")
|
||||
}
|
||||
|
||||
if m != OAuth2PKCECodeChallengeMethodS256 {
|
||||
return xerrors.Errorf("unsupported code_challenge_method: %s", method)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
Generated
+30
-34
@@ -20,15 +20,15 @@ curl -X GET http://coder-server:8080/api/v2/.well-known/oauth-authorization-serv
|
||||
{
|
||||
"authorization_endpoint": "string",
|
||||
"code_challenge_methods_supported": [
|
||||
"string"
|
||||
"S256"
|
||||
],
|
||||
"grant_types_supported": [
|
||||
"string"
|
||||
"authorization_code"
|
||||
],
|
||||
"issuer": "string",
|
||||
"registration_endpoint": "string",
|
||||
"response_types_supported": [
|
||||
"string"
|
||||
"code"
|
||||
],
|
||||
"revocation_endpoint": "string",
|
||||
"scopes_supported": [
|
||||
@@ -36,7 +36,7 @@ curl -X GET http://coder-server:8080/api/v2/.well-known/oauth-authorization-serv
|
||||
],
|
||||
"token_endpoint": "string",
|
||||
"token_endpoint_auth_methods_supported": [
|
||||
"string"
|
||||
"client_secret_basic"
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -1265,9 +1265,9 @@ curl -X GET http://coder-server:8080/api/v2/oauth2/authorize?client_id=string&st
|
||||
|
||||
#### Enumerated Values
|
||||
|
||||
| Parameter | Value(s) |
|
||||
|-----------------|----------|
|
||||
| `response_type` | `code` |
|
||||
| Parameter | Value(s) |
|
||||
|-----------------|-----------------|
|
||||
| `response_type` | `code`, `token` |
|
||||
|
||||
### Responses
|
||||
|
||||
@@ -1301,9 +1301,9 @@ curl -X POST http://coder-server:8080/api/v2/oauth2/authorize?client_id=string&s
|
||||
|
||||
#### Enumerated Values
|
||||
|
||||
| Parameter | Value(s) |
|
||||
|-----------------|----------|
|
||||
| `response_type` | `code` |
|
||||
| Parameter | Value(s) |
|
||||
|-----------------|-----------------|
|
||||
| `response_type` | `code`, `token` |
|
||||
|
||||
### Responses
|
||||
|
||||
@@ -1346,7 +1346,7 @@ curl -X GET http://coder-server:8080/api/v2/oauth2/clients/{client_id} \
|
||||
"string"
|
||||
],
|
||||
"grant_types": [
|
||||
"string"
|
||||
"authorization_code"
|
||||
],
|
||||
"jwks": {},
|
||||
"jwks_uri": "string",
|
||||
@@ -1355,17 +1355,15 @@ curl -X GET http://coder-server:8080/api/v2/oauth2/clients/{client_id} \
|
||||
"redirect_uris": [
|
||||
"string"
|
||||
],
|
||||
"registration_access_token": [
|
||||
0
|
||||
],
|
||||
"registration_access_token": "string",
|
||||
"registration_client_uri": "string",
|
||||
"response_types": [
|
||||
"string"
|
||||
"code"
|
||||
],
|
||||
"scope": "string",
|
||||
"software_id": "string",
|
||||
"software_version": "string",
|
||||
"token_endpoint_auth_method": "string",
|
||||
"token_endpoint_auth_method": "client_secret_basic",
|
||||
"tos_uri": "string"
|
||||
}
|
||||
```
|
||||
@@ -1399,7 +1397,7 @@ curl -X PUT http://coder-server:8080/api/v2/oauth2/clients/{client_id} \
|
||||
"string"
|
||||
],
|
||||
"grant_types": [
|
||||
"string"
|
||||
"authorization_code"
|
||||
],
|
||||
"jwks": {},
|
||||
"jwks_uri": "string",
|
||||
@@ -1409,13 +1407,13 @@ curl -X PUT http://coder-server:8080/api/v2/oauth2/clients/{client_id} \
|
||||
"string"
|
||||
],
|
||||
"response_types": [
|
||||
"string"
|
||||
"code"
|
||||
],
|
||||
"scope": "string",
|
||||
"software_id": "string",
|
||||
"software_statement": "string",
|
||||
"software_version": "string",
|
||||
"token_endpoint_auth_method": "string",
|
||||
"token_endpoint_auth_method": "client_secret_basic",
|
||||
"tos_uri": "string"
|
||||
}
|
||||
```
|
||||
@@ -1442,7 +1440,7 @@ curl -X PUT http://coder-server:8080/api/v2/oauth2/clients/{client_id} \
|
||||
"string"
|
||||
],
|
||||
"grant_types": [
|
||||
"string"
|
||||
"authorization_code"
|
||||
],
|
||||
"jwks": {},
|
||||
"jwks_uri": "string",
|
||||
@@ -1451,17 +1449,15 @@ curl -X PUT http://coder-server:8080/api/v2/oauth2/clients/{client_id} \
|
||||
"redirect_uris": [
|
||||
"string"
|
||||
],
|
||||
"registration_access_token": [
|
||||
0
|
||||
],
|
||||
"registration_access_token": "string",
|
||||
"registration_client_uri": "string",
|
||||
"response_types": [
|
||||
"string"
|
||||
"code"
|
||||
],
|
||||
"scope": "string",
|
||||
"software_id": "string",
|
||||
"software_version": "string",
|
||||
"token_endpoint_auth_method": "string",
|
||||
"token_endpoint_auth_method": "client_secret_basic",
|
||||
"tos_uri": "string"
|
||||
}
|
||||
```
|
||||
@@ -1519,7 +1515,7 @@ curl -X POST http://coder-server:8080/api/v2/oauth2/register \
|
||||
"string"
|
||||
],
|
||||
"grant_types": [
|
||||
"string"
|
||||
"authorization_code"
|
||||
],
|
||||
"jwks": {},
|
||||
"jwks_uri": "string",
|
||||
@@ -1529,13 +1525,13 @@ curl -X POST http://coder-server:8080/api/v2/oauth2/register \
|
||||
"string"
|
||||
],
|
||||
"response_types": [
|
||||
"string"
|
||||
"code"
|
||||
],
|
||||
"scope": "string",
|
||||
"software_id": "string",
|
||||
"software_statement": "string",
|
||||
"software_version": "string",
|
||||
"token_endpoint_auth_method": "string",
|
||||
"token_endpoint_auth_method": "client_secret_basic",
|
||||
"tos_uri": "string"
|
||||
}
|
||||
```
|
||||
@@ -1562,7 +1558,7 @@ curl -X POST http://coder-server:8080/api/v2/oauth2/register \
|
||||
"string"
|
||||
],
|
||||
"grant_types": [
|
||||
"string"
|
||||
"authorization_code"
|
||||
],
|
||||
"jwks": {},
|
||||
"jwks_uri": "string",
|
||||
@@ -1574,12 +1570,12 @@ curl -X POST http://coder-server:8080/api/v2/oauth2/register \
|
||||
"registration_access_token": "string",
|
||||
"registration_client_uri": "string",
|
||||
"response_types": [
|
||||
"string"
|
||||
"code"
|
||||
],
|
||||
"scope": "string",
|
||||
"software_id": "string",
|
||||
"software_version": "string",
|
||||
"token_endpoint_auth_method": "string",
|
||||
"token_endpoint_auth_method": "client_secret_basic",
|
||||
"tos_uri": "string"
|
||||
}
|
||||
```
|
||||
@@ -1662,9 +1658,9 @@ grant_type: authorization_code
|
||||
|
||||
#### Enumerated Values
|
||||
|
||||
| Parameter | Value(s) |
|
||||
|----------------|---------------------------------------|
|
||||
| `» grant_type` | `authorization_code`, `refresh_token` |
|
||||
| Parameter | Value(s) |
|
||||
|----------------|-------------------------------------------------------------------------------------|
|
||||
| `» grant_type` | `authorization_code`, `client_credentials`, `implicit`, `password`, `refresh_token` |
|
||||
|
||||
### Example responses
|
||||
|
||||
|
||||
Generated
+145
-91
@@ -5188,15 +5188,15 @@ Only certain features set these fields: - FeatureManagedAgentLimit|
|
||||
{
|
||||
"authorization_endpoint": "string",
|
||||
"code_challenge_methods_supported": [
|
||||
"string"
|
||||
"S256"
|
||||
],
|
||||
"grant_types_supported": [
|
||||
"string"
|
||||
"authorization_code"
|
||||
],
|
||||
"issuer": "string",
|
||||
"registration_endpoint": "string",
|
||||
"response_types_supported": [
|
||||
"string"
|
||||
"code"
|
||||
],
|
||||
"revocation_endpoint": "string",
|
||||
"scopes_supported": [
|
||||
@@ -5204,25 +5204,25 @@ Only certain features set these fields: - FeatureManagedAgentLimit|
|
||||
],
|
||||
"token_endpoint": "string",
|
||||
"token_endpoint_auth_methods_supported": [
|
||||
"string"
|
||||
"client_secret_basic"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
|-----------------------------------------|-----------------|----------|--------------|-------------|
|
||||
| `authorization_endpoint` | string | false | | |
|
||||
| `code_challenge_methods_supported` | array of string | false | | |
|
||||
| `grant_types_supported` | array of string | false | | |
|
||||
| `issuer` | string | false | | |
|
||||
| `registration_endpoint` | string | false | | |
|
||||
| `response_types_supported` | array of string | false | | |
|
||||
| `revocation_endpoint` | 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 [codersdk.OAuth2PKCECodeChallengeMethod](#codersdkoauth2pkcecodechallengemethod) | false | | |
|
||||
| `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 | | |
|
||||
| `revocation_endpoint` | string | false | | |
|
||||
| `scopes_supported` | array of string | false | | |
|
||||
| `token_endpoint` | string | false | | |
|
||||
| `token_endpoint_auth_methods_supported` | array of [codersdk.OAuth2TokenEndpointAuthMethod](#codersdkoauth2tokenendpointauthmethod) | false | | |
|
||||
|
||||
## codersdk.OAuth2ClientConfiguration
|
||||
|
||||
@@ -5237,7 +5237,7 @@ Only certain features set these fields: - FeatureManagedAgentLimit|
|
||||
"string"
|
||||
],
|
||||
"grant_types": [
|
||||
"string"
|
||||
"authorization_code"
|
||||
],
|
||||
"jwks": {},
|
||||
"jwks_uri": "string",
|
||||
@@ -5246,45 +5246,43 @@ Only certain features set these fields: - FeatureManagedAgentLimit|
|
||||
"redirect_uris": [
|
||||
"string"
|
||||
],
|
||||
"registration_access_token": [
|
||||
0
|
||||
],
|
||||
"registration_access_token": "string",
|
||||
"registration_client_uri": "string",
|
||||
"response_types": [
|
||||
"string"
|
||||
"code"
|
||||
],
|
||||
"scope": "string",
|
||||
"software_id": "string",
|
||||
"software_version": "string",
|
||||
"token_endpoint_auth_method": "string",
|
||||
"token_endpoint_auth_method": "client_secret_basic",
|
||||
"tos_uri": "string"
|
||||
}
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
|------------------------------|------------------|----------|--------------|-------------|
|
||||
| `client_id` | string | false | | |
|
||||
| `client_id_issued_at` | integer | false | | |
|
||||
| `client_name` | string | false | | |
|
||||
| `client_secret_expires_at` | integer | false | | |
|
||||
| `client_uri` | string | false | | |
|
||||
| `contacts` | array of string | false | | |
|
||||
| `grant_types` | array of string | false | | |
|
||||
| `jwks` | object | false | | |
|
||||
| `jwks_uri` | string | false | | |
|
||||
| `logo_uri` | string | false | | |
|
||||
| `policy_uri` | string | false | | |
|
||||
| `redirect_uris` | array of string | false | | |
|
||||
| `registration_access_token` | array of integer | false | | |
|
||||
| `registration_client_uri` | string | false | | |
|
||||
| `response_types` | array of string | false | | |
|
||||
| `scope` | string | false | | |
|
||||
| `software_id` | string | false | | |
|
||||
| `software_version` | string | false | | |
|
||||
| `token_endpoint_auth_method` | string | false | | |
|
||||
| `tos_uri` | string | false | | |
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
|------------------------------|-------------------------------------------------------------------------------------|----------|--------------|-------------|
|
||||
| `client_id` | string | false | | |
|
||||
| `client_id_issued_at` | integer | false | | |
|
||||
| `client_name` | string | false | | |
|
||||
| `client_secret_expires_at` | integer | false | | |
|
||||
| `client_uri` | string | false | | |
|
||||
| `contacts` | array of string | false | | |
|
||||
| `grant_types` | array of [codersdk.OAuth2ProviderGrantType](#codersdkoauth2providergranttype) | false | | |
|
||||
| `jwks` | object | false | | |
|
||||
| `jwks_uri` | string | false | | |
|
||||
| `logo_uri` | string | false | | |
|
||||
| `policy_uri` | string | false | | |
|
||||
| `redirect_uris` | array of string | false | | |
|
||||
| `registration_access_token` | string | false | | |
|
||||
| `registration_client_uri` | string | false | | |
|
||||
| `response_types` | array of [codersdk.OAuth2ProviderResponseType](#codersdkoauth2providerresponsetype) | false | | |
|
||||
| `scope` | string | false | | |
|
||||
| `software_id` | string | false | | |
|
||||
| `software_version` | string | false | | |
|
||||
| `token_endpoint_auth_method` | [codersdk.OAuth2TokenEndpointAuthMethod](#codersdkoauth2tokenendpointauthmethod) | false | | |
|
||||
| `tos_uri` | string | false | | |
|
||||
|
||||
## codersdk.OAuth2ClientRegistrationRequest
|
||||
|
||||
@@ -5296,7 +5294,7 @@ Only certain features set these fields: - FeatureManagedAgentLimit|
|
||||
"string"
|
||||
],
|
||||
"grant_types": [
|
||||
"string"
|
||||
"authorization_code"
|
||||
],
|
||||
"jwks": {},
|
||||
"jwks_uri": "string",
|
||||
@@ -5306,37 +5304,37 @@ Only certain features set these fields: - FeatureManagedAgentLimit|
|
||||
"string"
|
||||
],
|
||||
"response_types": [
|
||||
"string"
|
||||
"code"
|
||||
],
|
||||
"scope": "string",
|
||||
"software_id": "string",
|
||||
"software_statement": "string",
|
||||
"software_version": "string",
|
||||
"token_endpoint_auth_method": "string",
|
||||
"token_endpoint_auth_method": "client_secret_basic",
|
||||
"tos_uri": "string"
|
||||
}
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
|------------------------------|-----------------|----------|--------------|-------------|
|
||||
| `client_name` | string | false | | |
|
||||
| `client_uri` | string | false | | |
|
||||
| `contacts` | array of string | false | | |
|
||||
| `grant_types` | array of string | false | | |
|
||||
| `jwks` | object | false | | |
|
||||
| `jwks_uri` | string | false | | |
|
||||
| `logo_uri` | string | false | | |
|
||||
| `policy_uri` | string | false | | |
|
||||
| `redirect_uris` | array of string | false | | |
|
||||
| `response_types` | array of string | false | | |
|
||||
| `scope` | string | false | | |
|
||||
| `software_id` | string | false | | |
|
||||
| `software_statement` | string | false | | |
|
||||
| `software_version` | string | false | | |
|
||||
| `token_endpoint_auth_method` | string | false | | |
|
||||
| `tos_uri` | string | false | | |
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
|------------------------------|-------------------------------------------------------------------------------------|----------|--------------|-------------|
|
||||
| `client_name` | string | false | | |
|
||||
| `client_uri` | string | false | | |
|
||||
| `contacts` | array of string | false | | |
|
||||
| `grant_types` | array of [codersdk.OAuth2ProviderGrantType](#codersdkoauth2providergranttype) | false | | |
|
||||
| `jwks` | object | false | | |
|
||||
| `jwks_uri` | string | false | | |
|
||||
| `logo_uri` | string | false | | |
|
||||
| `policy_uri` | string | false | | |
|
||||
| `redirect_uris` | array of string | false | | |
|
||||
| `response_types` | array of [codersdk.OAuth2ProviderResponseType](#codersdkoauth2providerresponsetype) | false | | |
|
||||
| `scope` | string | false | | |
|
||||
| `software_id` | string | false | | |
|
||||
| `software_statement` | string | false | | |
|
||||
| `software_version` | string | false | | |
|
||||
| `token_endpoint_auth_method` | [codersdk.OAuth2TokenEndpointAuthMethod](#codersdkoauth2tokenendpointauthmethod) | false | | |
|
||||
| `tos_uri` | string | false | | |
|
||||
|
||||
## codersdk.OAuth2ClientRegistrationResponse
|
||||
|
||||
@@ -5352,7 +5350,7 @@ Only certain features set these fields: - FeatureManagedAgentLimit|
|
||||
"string"
|
||||
],
|
||||
"grant_types": [
|
||||
"string"
|
||||
"authorization_code"
|
||||
],
|
||||
"jwks": {},
|
||||
"jwks_uri": "string",
|
||||
@@ -5364,41 +5362,41 @@ Only certain features set these fields: - FeatureManagedAgentLimit|
|
||||
"registration_access_token": "string",
|
||||
"registration_client_uri": "string",
|
||||
"response_types": [
|
||||
"string"
|
||||
"code"
|
||||
],
|
||||
"scope": "string",
|
||||
"software_id": "string",
|
||||
"software_version": "string",
|
||||
"token_endpoint_auth_method": "string",
|
||||
"token_endpoint_auth_method": "client_secret_basic",
|
||||
"tos_uri": "string"
|
||||
}
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
|------------------------------|-----------------|----------|--------------|-------------|
|
||||
| `client_id` | string | false | | |
|
||||
| `client_id_issued_at` | integer | false | | |
|
||||
| `client_name` | string | false | | |
|
||||
| `client_secret` | string | false | | |
|
||||
| `client_secret_expires_at` | integer | false | | |
|
||||
| `client_uri` | string | false | | |
|
||||
| `contacts` | array of string | false | | |
|
||||
| `grant_types` | array of string | false | | |
|
||||
| `jwks` | object | false | | |
|
||||
| `jwks_uri` | string | false | | |
|
||||
| `logo_uri` | string | false | | |
|
||||
| `policy_uri` | string | false | | |
|
||||
| `redirect_uris` | array of string | false | | |
|
||||
| `registration_access_token` | string | false | | |
|
||||
| `registration_client_uri` | string | false | | |
|
||||
| `response_types` | array of string | false | | |
|
||||
| `scope` | string | false | | |
|
||||
| `software_id` | string | false | | |
|
||||
| `software_version` | string | false | | |
|
||||
| `token_endpoint_auth_method` | string | false | | |
|
||||
| `tos_uri` | string | false | | |
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
|------------------------------|-------------------------------------------------------------------------------------|----------|--------------|-------------|
|
||||
| `client_id` | string | false | | |
|
||||
| `client_id_issued_at` | integer | false | | |
|
||||
| `client_name` | string | false | | |
|
||||
| `client_secret` | string | false | | |
|
||||
| `client_secret_expires_at` | integer | false | | |
|
||||
| `client_uri` | string | false | | |
|
||||
| `contacts` | array of string | false | | |
|
||||
| `grant_types` | array of [codersdk.OAuth2ProviderGrantType](#codersdkoauth2providergranttype) | false | | |
|
||||
| `jwks` | object | false | | |
|
||||
| `jwks_uri` | string | false | | |
|
||||
| `logo_uri` | string | false | | |
|
||||
| `policy_uri` | string | false | | |
|
||||
| `redirect_uris` | array of string | false | | |
|
||||
| `registration_access_token` | string | false | | |
|
||||
| `registration_client_uri` | string | false | | |
|
||||
| `response_types` | array of [codersdk.OAuth2ProviderResponseType](#codersdkoauth2providerresponsetype) | false | | |
|
||||
| `scope` | string | false | | |
|
||||
| `software_id` | string | false | | |
|
||||
| `software_version` | string | false | | |
|
||||
| `token_endpoint_auth_method` | [codersdk.OAuth2TokenEndpointAuthMethod](#codersdkoauth2tokenendpointauthmethod) | false | | |
|
||||
| `tos_uri` | string | false | | |
|
||||
|
||||
## codersdk.OAuth2Config
|
||||
|
||||
@@ -5462,6 +5460,20 @@ Only certain features set these fields: - FeatureManagedAgentLimit|
|
||||
| `device_flow` | boolean | false | | |
|
||||
| `enterprise_base_url` | string | false | | |
|
||||
|
||||
## codersdk.OAuth2PKCECodeChallengeMethod
|
||||
|
||||
```json
|
||||
"S256"
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
#### Enumerated Values
|
||||
|
||||
| Value(s) |
|
||||
|-----------------|
|
||||
| `S256`, `plain` |
|
||||
|
||||
## codersdk.OAuth2ProtectedResourceMetadata
|
||||
|
||||
```json
|
||||
@@ -5549,6 +5561,48 @@ Only certain features set these fields: - FeatureManagedAgentLimit|
|
||||
| `client_secret_full` | string | false | | |
|
||||
| `id` | string | false | | |
|
||||
|
||||
## codersdk.OAuth2ProviderGrantType
|
||||
|
||||
```json
|
||||
"authorization_code"
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
#### Enumerated Values
|
||||
|
||||
| Value(s) |
|
||||
|-------------------------------------------------------------------------------------|
|
||||
| `authorization_code`, `client_credentials`, `implicit`, `password`, `refresh_token` |
|
||||
|
||||
## codersdk.OAuth2ProviderResponseType
|
||||
|
||||
```json
|
||||
"code"
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
#### Enumerated Values
|
||||
|
||||
| Value(s) |
|
||||
|-----------------|
|
||||
| `code`, `token` |
|
||||
|
||||
## codersdk.OAuth2TokenEndpointAuthMethod
|
||||
|
||||
```json
|
||||
"client_secret_basic"
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
#### Enumerated Values
|
||||
|
||||
| Value(s) |
|
||||
|-----------------------------------------------------|
|
||||
| `client_secret_basic`, `client_secret_post`, `none` |
|
||||
|
||||
## codersdk.OAuthConversionResponse
|
||||
|
||||
```json
|
||||
|
||||
Generated
+148
-22
@@ -2934,7 +2934,7 @@ export interface OAuth2AppEndpoints {
|
||||
|
||||
// From codersdk/oauth2.go
|
||||
/**
|
||||
* OAuth2AuthorizationServerMetadata represents RFC 8414 OAuth 2.0 Authorization Server Metadata
|
||||
* OAuth2AuthorizationServerMetadata represents RFC 8414 OAuth 2.0 Authorization Server Metadata.
|
||||
*/
|
||||
export interface OAuth2AuthorizationServerMetadata {
|
||||
readonly issuer: string;
|
||||
@@ -2942,17 +2942,16 @@ export interface OAuth2AuthorizationServerMetadata {
|
||||
readonly token_endpoint: string;
|
||||
readonly registration_endpoint?: string;
|
||||
readonly revocation_endpoint?: string;
|
||||
readonly response_types_supported: readonly string[];
|
||||
readonly grant_types_supported: readonly string[];
|
||||
readonly code_challenge_methods_supported: readonly string[];
|
||||
readonly response_types_supported: readonly OAuth2ProviderResponseType[];
|
||||
readonly grant_types_supported?: readonly OAuth2ProviderGrantType[];
|
||||
readonly code_challenge_methods_supported?: readonly OAuth2PKCECodeChallengeMethod[];
|
||||
readonly scopes_supported?: readonly string[];
|
||||
readonly token_endpoint_auth_methods_supported?: readonly string[];
|
||||
readonly token_endpoint_auth_methods_supported?: readonly OAuth2TokenEndpointAuthMethod[];
|
||||
}
|
||||
|
||||
// From codersdk/oauth2.go
|
||||
/**
|
||||
* OAuth2ClientConfiguration represents RFC 7592 Client Configuration (for GET/PUT operations)
|
||||
* Same as OAuth2ClientRegistrationResponse but without client_secret in GET responses
|
||||
* OAuth2ClientConfiguration represents RFC 7592 Client Read Response.
|
||||
*/
|
||||
export interface OAuth2ClientConfiguration {
|
||||
readonly client_id: string;
|
||||
@@ -2968,18 +2967,18 @@ export interface OAuth2ClientConfiguration {
|
||||
readonly jwks?: Record<string, string>;
|
||||
readonly software_id?: string;
|
||||
readonly software_version?: string;
|
||||
readonly grant_types: readonly string[];
|
||||
readonly response_types: readonly string[];
|
||||
readonly token_endpoint_auth_method: string;
|
||||
readonly grant_types: readonly OAuth2ProviderGrantType[];
|
||||
readonly response_types: readonly OAuth2ProviderResponseType[];
|
||||
readonly token_endpoint_auth_method: OAuth2TokenEndpointAuthMethod;
|
||||
readonly scope?: string;
|
||||
readonly contacts?: readonly string[];
|
||||
readonly registration_access_token: string;
|
||||
readonly registration_access_token?: string;
|
||||
readonly registration_client_uri: string;
|
||||
}
|
||||
|
||||
// From codersdk/oauth2.go
|
||||
/**
|
||||
* OAuth2ClientRegistrationRequest represents RFC 7591 Dynamic Client Registration Request
|
||||
* OAuth2ClientRegistrationRequest represents RFC 7591 Dynamic Client Registration Request.
|
||||
*/
|
||||
export interface OAuth2ClientRegistrationRequest {
|
||||
readonly redirect_uris?: readonly string[];
|
||||
@@ -2993,21 +2992,21 @@ export interface OAuth2ClientRegistrationRequest {
|
||||
readonly software_id?: string;
|
||||
readonly software_version?: string;
|
||||
readonly software_statement?: string;
|
||||
readonly grant_types?: readonly string[];
|
||||
readonly response_types?: readonly string[];
|
||||
readonly token_endpoint_auth_method?: string;
|
||||
readonly grant_types?: readonly OAuth2ProviderGrantType[];
|
||||
readonly response_types?: readonly OAuth2ProviderResponseType[];
|
||||
readonly token_endpoint_auth_method?: OAuth2TokenEndpointAuthMethod;
|
||||
readonly scope?: string;
|
||||
readonly contacts?: readonly string[];
|
||||
}
|
||||
|
||||
// From codersdk/oauth2.go
|
||||
/**
|
||||
* OAuth2ClientRegistrationResponse represents RFC 7591 Dynamic Client Registration Response
|
||||
* OAuth2ClientRegistrationResponse represents RFC 7591 Dynamic Client Registration Response.
|
||||
*/
|
||||
export interface OAuth2ClientRegistrationResponse {
|
||||
readonly client_id: string;
|
||||
readonly client_secret?: string;
|
||||
readonly client_id_issued_at: number;
|
||||
readonly client_id_issued_at?: number;
|
||||
readonly client_secret_expires_at?: number;
|
||||
readonly redirect_uris?: readonly string[];
|
||||
readonly client_name?: string;
|
||||
@@ -3019,9 +3018,9 @@ export interface OAuth2ClientRegistrationResponse {
|
||||
readonly jwks?: Record<string, string>;
|
||||
readonly software_id?: string;
|
||||
readonly software_version?: string;
|
||||
readonly grant_types: readonly string[];
|
||||
readonly response_types: readonly string[];
|
||||
readonly token_endpoint_auth_method: string;
|
||||
readonly grant_types: readonly OAuth2ProviderGrantType[];
|
||||
readonly response_types: readonly OAuth2ProviderResponseType[];
|
||||
readonly token_endpoint_auth_method: OAuth2TokenEndpointAuthMethod;
|
||||
readonly scope?: string;
|
||||
readonly contacts?: readonly string[];
|
||||
readonly registration_access_token: string;
|
||||
@@ -3038,6 +3037,46 @@ export interface OAuth2DeviceFlowCallbackResponse {
|
||||
readonly redirect_url: string;
|
||||
}
|
||||
|
||||
// From codersdk/oauth2.go
|
||||
/**
|
||||
* OAuth2Error represents an OAuth2-compliant error response per RFC 6749.
|
||||
*/
|
||||
export interface OAuth2Error {
|
||||
readonly error: OAuth2ErrorCode;
|
||||
readonly error_description?: string;
|
||||
readonly error_uri?: string;
|
||||
}
|
||||
|
||||
// From codersdk/oauth2.go
|
||||
export type OAuth2ErrorCode =
|
||||
| "access_denied"
|
||||
| "invalid_client"
|
||||
| "invalid_grant"
|
||||
| "invalid_request"
|
||||
| "invalid_scope"
|
||||
| "invalid_target"
|
||||
| "server_error"
|
||||
| "temporarily_unavailable"
|
||||
| "unauthorized_client"
|
||||
| "unsupported_grant_type"
|
||||
| "unsupported_response_type"
|
||||
| "unsupported_token_type";
|
||||
|
||||
export const OAuth2ErrorCodes: OAuth2ErrorCode[] = [
|
||||
"access_denied",
|
||||
"invalid_client",
|
||||
"invalid_grant",
|
||||
"invalid_request",
|
||||
"invalid_scope",
|
||||
"invalid_target",
|
||||
"server_error",
|
||||
"temporarily_unavailable",
|
||||
"unauthorized_client",
|
||||
"unsupported_grant_type",
|
||||
"unsupported_response_type",
|
||||
"unsupported_token_type",
|
||||
];
|
||||
|
||||
// From codersdk/deployment.go
|
||||
export interface OAuth2GithubConfig {
|
||||
readonly client_id: string;
|
||||
@@ -3051,6 +3090,14 @@ export interface OAuth2GithubConfig {
|
||||
readonly enterprise_base_url: string;
|
||||
}
|
||||
|
||||
// From codersdk/oauth2.go
|
||||
export type OAuth2PKCECodeChallengeMethod = "plain" | "S256";
|
||||
|
||||
export const OAuth2PKCECodeChallengeMethods: OAuth2PKCECodeChallengeMethod[] = [
|
||||
"plain",
|
||||
"S256",
|
||||
];
|
||||
|
||||
// From codersdk/client.go
|
||||
/**
|
||||
* OAuth2PKCEVerifier is the name of the cookie that stores the oauth2 PKCE
|
||||
@@ -3103,18 +3150,27 @@ export interface OAuth2ProviderAppSecretFull {
|
||||
}
|
||||
|
||||
// From codersdk/oauth2.go
|
||||
export type OAuth2ProviderGrantType = "authorization_code" | "refresh_token";
|
||||
export type OAuth2ProviderGrantType =
|
||||
| "authorization_code"
|
||||
| "client_credentials"
|
||||
| "implicit"
|
||||
| "password"
|
||||
| "refresh_token";
|
||||
|
||||
export const OAuth2ProviderGrantTypes: OAuth2ProviderGrantType[] = [
|
||||
"authorization_code",
|
||||
"client_credentials",
|
||||
"implicit",
|
||||
"password",
|
||||
"refresh_token",
|
||||
];
|
||||
|
||||
// From codersdk/oauth2.go
|
||||
export type OAuth2ProviderResponseType = "code";
|
||||
export type OAuth2ProviderResponseType = "code" | "token";
|
||||
|
||||
export const OAuth2ProviderResponseTypes: OAuth2ProviderResponseType[] = [
|
||||
"code",
|
||||
"token",
|
||||
];
|
||||
|
||||
// From codersdk/client.go
|
||||
@@ -3123,12 +3179,82 @@ export const OAuth2ProviderResponseTypes: OAuth2ProviderResponseType[] = [
|
||||
*/
|
||||
export const OAuth2RedirectCookie = "oauth_redirect";
|
||||
|
||||
// From codersdk/oauth2.go
|
||||
export type OAuth2RevocationTokenTypeHint = "access_token" | "refresh_token";
|
||||
|
||||
export const OAuth2RevocationTokenTypeHints: OAuth2RevocationTokenTypeHint[] = [
|
||||
"access_token",
|
||||
"refresh_token",
|
||||
];
|
||||
|
||||
// From codersdk/client.go
|
||||
/**
|
||||
* OAuth2StateCookie is the name of the cookie that stores the oauth2 state.
|
||||
*/
|
||||
export const OAuth2StateCookie = "oauth_state";
|
||||
|
||||
// From codersdk/oauth2.go
|
||||
export type OAuth2TokenEndpointAuthMethod =
|
||||
| "client_secret_basic"
|
||||
| "client_secret_post"
|
||||
| "none";
|
||||
|
||||
export const OAuth2TokenEndpointAuthMethods: OAuth2TokenEndpointAuthMethod[] = [
|
||||
"client_secret_basic",
|
||||
"client_secret_post",
|
||||
"none",
|
||||
];
|
||||
|
||||
// From codersdk/oauth2.go
|
||||
/**
|
||||
* OAuth2TokenRequest represents a token request per RFC 6749. The actual wire
|
||||
* format is application/x-www-form-urlencoded; this struct is for SDK docs.
|
||||
*/
|
||||
export interface OAuth2TokenRequest {
|
||||
readonly grant_type: OAuth2ProviderGrantType;
|
||||
readonly code?: string;
|
||||
readonly redirect_uri?: string;
|
||||
readonly client_id?: string;
|
||||
readonly client_secret?: string;
|
||||
readonly code_verifier?: string;
|
||||
readonly refresh_token?: string;
|
||||
readonly resource?: string;
|
||||
readonly scope?: string;
|
||||
}
|
||||
|
||||
// From codersdk/oauth2.go
|
||||
/**
|
||||
* OAuth2TokenResponse represents a successful token response per RFC 6749.
|
||||
*/
|
||||
export interface OAuth2TokenResponse {
|
||||
readonly access_token: string;
|
||||
readonly token_type: OAuth2TokenType;
|
||||
readonly expires_in?: number;
|
||||
readonly refresh_token?: string;
|
||||
readonly scope?: string;
|
||||
/**
|
||||
* Expiry is not part of RFC 6749 but is included for compatibility with
|
||||
* golang.org/x/oauth2.Token and clients that expect a timestamp.
|
||||
*/
|
||||
readonly expiry?: string;
|
||||
}
|
||||
|
||||
// From codersdk/oauth2.go
|
||||
/**
|
||||
* OAuth2TokenRevocationRequest represents a token revocation request per RFC 7009.
|
||||
*/
|
||||
export interface OAuth2TokenRevocationRequest {
|
||||
readonly token: string;
|
||||
readonly token_type_hint?: OAuth2RevocationTokenTypeHint;
|
||||
readonly client_id?: string;
|
||||
readonly client_secret?: string;
|
||||
}
|
||||
|
||||
// From codersdk/oauth2.go
|
||||
export type OAuth2TokenType = "Bearer" | "DPoP";
|
||||
|
||||
export const OAuth2TokenTypes: OAuth2TokenType[] = ["Bearer", "DPoP"];
|
||||
|
||||
// From codersdk/users.go
|
||||
export interface OAuthConversionResponse {
|
||||
readonly state_string: string;
|
||||
|
||||
Reference in New Issue
Block a user