-
Notifications
You must be signed in to change notification settings - Fork 3
Add oauth package with RFC-compliant OAuth/OIDC utilities #8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| // SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| package oauth | ||
|
|
||
| // Well-known endpoint paths as defined by RFC 8414, OpenID Connect Discovery 1.0, and RFC 9728. | ||
| const ( | ||
| // WellKnownOIDCPath is the standard OIDC discovery endpoint path | ||
| // per OpenID Connect Discovery 1.0 specification. | ||
| WellKnownOIDCPath = "/.well-known/openid-configuration" | ||
|
|
||
| // WellKnownOAuthServerPath is the standard OAuth authorization server metadata endpoint path | ||
| // per RFC 8414 (OAuth 2.0 Authorization Server Metadata). | ||
| WellKnownOAuthServerPath = "/.well-known/oauth-authorization-server" | ||
|
|
||
| // WellKnownOAuthResourcePath is the RFC 9728 standard path for OAuth Protected Resource metadata. | ||
| // Per RFC 9728 Section 3, this endpoint and any subpaths under it should be accessible | ||
| // without authentication to enable OIDC/OAuth discovery. | ||
| WellKnownOAuthResourcePath = "/.well-known/oauth-protected-resource" | ||
| ) | ||
|
|
||
| // Grant types as defined by RFC 6749. | ||
| const ( | ||
| // GrantTypeAuthorizationCode is the authorization code grant type (RFC 6749 Section 4.1). | ||
| GrantTypeAuthorizationCode = "authorization_code" | ||
|
|
||
| // GrantTypeRefreshToken is the refresh token grant type (RFC 6749 Section 6). | ||
| GrantTypeRefreshToken = "refresh_token" | ||
| ) | ||
|
|
||
| // Response types as defined by RFC 6749. | ||
| const ( | ||
| // ResponseTypeCode is the authorization code response type (RFC 6749 Section 4.1.1). | ||
| ResponseTypeCode = "code" | ||
| ) | ||
|
|
||
| // Token endpoint authentication methods as defined by RFC 7591. | ||
| const ( | ||
| // TokenEndpointAuthMethodNone indicates no client authentication (public clients). | ||
| // Typically used with PKCE for native/mobile applications. | ||
| TokenEndpointAuthMethodNone = "none" | ||
| ) | ||
|
|
||
| // PKCE (Proof Key for Code Exchange) methods as defined by RFC 7636. | ||
| const ( | ||
| // PKCEMethodS256 uses SHA-256 hash of the code verifier (recommended). | ||
| PKCEMethodS256 = "S256" | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| // SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| package oauth | ||
|
|
||
| // AuthorizationServerMetadata represents the OAuth 2.0 Authorization Server Metadata | ||
| // per RFC 8414. This is the base structure that OIDC Discovery extends. | ||
| type AuthorizationServerMetadata struct { | ||
| // Issuer is the authorization server's issuer identifier (REQUIRED per RFC 8414). | ||
| Issuer string `json:"issuer"` | ||
|
|
||
| // AuthorizationEndpoint is the URL of the authorization endpoint (RECOMMENDED). | ||
| // Note: No omitempty to maintain backward compatibility with existing JSON serialization. | ||
| AuthorizationEndpoint string `json:"authorization_endpoint"` | ||
|
|
||
| // TokenEndpoint is the URL of the token endpoint (RECOMMENDED). | ||
| // Note: No omitempty to maintain backward compatibility with existing JSON serialization. | ||
| TokenEndpoint string `json:"token_endpoint"` | ||
|
|
||
| // JWKSURI is the URL of the JSON Web Key Set document (RECOMMENDED). | ||
| // Note: No omitempty to maintain backward compatibility with existing JSON serialization. | ||
| JWKSURI string `json:"jwks_uri"` | ||
|
|
||
| // RegistrationEndpoint is the URL of the Dynamic Client Registration endpoint (OPTIONAL). | ||
| RegistrationEndpoint string `json:"registration_endpoint,omitempty"` | ||
|
|
||
| // IntrospectionEndpoint is the URL of the token introspection endpoint (OPTIONAL, RFC 7662). | ||
| IntrospectionEndpoint string `json:"introspection_endpoint,omitempty"` | ||
|
|
||
| // UserinfoEndpoint is the URL of the UserInfo endpoint (OPTIONAL, OIDC specific). | ||
| // Note: No omitempty to maintain backward compatibility with existing JSON serialization. | ||
| UserinfoEndpoint string `json:"userinfo_endpoint"` | ||
|
|
||
| // ResponseTypesSupported lists the response types supported (RECOMMENDED). | ||
| ResponseTypesSupported []string `json:"response_types_supported,omitempty"` | ||
|
|
||
| // GrantTypesSupported lists the grant types supported (OPTIONAL). | ||
| GrantTypesSupported []string `json:"grant_types_supported,omitempty"` | ||
|
|
||
| // CodeChallengeMethodsSupported lists the PKCE code challenge methods supported (OPTIONAL). | ||
| CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported,omitempty"` | ||
|
|
||
| // TokenEndpointAuthMethodsSupported lists the authentication methods supported at the token endpoint (OPTIONAL). | ||
| TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported,omitempty"` | ||
|
|
||
| // ScopesSupported lists the OAuth 2.0 scope values supported (RECOMMENDED per RFC 8414). | ||
| // For MCP authorization servers, this typically includes "openid" and "offline_access". | ||
| ScopesSupported []string `json:"scopes_supported,omitempty"` | ||
| } | ||
|
|
||
| // OIDCDiscoveryDocument represents the OpenID Connect Discovery 1.0 document. | ||
| // It extends OAuth 2.0 Authorization Server Metadata (RFC 8414) with OIDC-specific fields. | ||
| // This unified type supports both producer (server) and consumer (client) use cases. | ||
| type OIDCDiscoveryDocument struct { | ||
| // Embed OAuth 2.0 AS Metadata (RFC 8414) as the base | ||
| AuthorizationServerMetadata | ||
|
|
||
| // SubjectTypesSupported lists the subject identifier types supported (REQUIRED for OIDC). | ||
| SubjectTypesSupported []string `json:"subject_types_supported,omitempty"` | ||
|
|
||
| // IDTokenSigningAlgValuesSupported lists the JWS algorithms supported for ID tokens (REQUIRED for OIDC). | ||
| IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported,omitempty"` | ||
|
|
||
| // ClaimsSupported lists the claims that can be returned (RECOMMENDED for OIDC). | ||
| ClaimsSupported []string `json:"claims_supported,omitempty"` | ||
| } | ||
|
|
||
| // Validate performs basic validation on the discovery document. | ||
| // It checks for required fields based on whether this is an OIDC or pure OAuth document. | ||
| func (d *OIDCDiscoveryDocument) Validate(isOIDC bool) error { | ||
| if d.Issuer == "" { | ||
| return ErrMissingIssuer | ||
| } | ||
| if d.AuthorizationEndpoint == "" { | ||
| return ErrMissingAuthorizationEndpoint | ||
| } | ||
| if d.TokenEndpoint == "" { | ||
| return ErrMissingTokenEndpoint | ||
| } | ||
| if isOIDC && d.JWKSURI == "" { | ||
| return ErrMissingJWKSURI | ||
| } | ||
| if isOIDC && len(d.ResponseTypesSupported) == 0 { | ||
| return ErrMissingResponseTypesSupported | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| // SupportsPKCE returns true if the authorization server supports PKCE with S256. | ||
| func (d *OIDCDiscoveryDocument) SupportsPKCE() bool { | ||
| for _, method := range d.CodeChallengeMethodsSupported { | ||
| if method == PKCEMethodS256 { | ||
| return true | ||
| } | ||
| } | ||
| return false | ||
| } | ||
|
|
||
| // SupportsGrantType returns true if the authorization server supports the given grant type. | ||
| func (d *OIDCDiscoveryDocument) SupportsGrantType(grantType string) bool { | ||
| for _, gt := range d.GrantTypesSupported { | ||
| if gt == grantType { | ||
| return true | ||
| } | ||
| } | ||
| return false | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,116 @@ | ||
| // SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| package oauth | ||
|
|
||
| import ( | ||
| "errors" | ||
| "testing" | ||
| ) | ||
|
|
||
| func TestOIDCDiscoveryDocument_Validate(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| validDoc := func() OIDCDiscoveryDocument { | ||
| return OIDCDiscoveryDocument{ | ||
| AuthorizationServerMetadata: AuthorizationServerMetadata{ | ||
| Issuer: "https://example.com", | ||
| AuthorizationEndpoint: "https://example.com/authorize", | ||
| TokenEndpoint: "https://example.com/token", | ||
| JWKSURI: "https://example.com/jwks", | ||
| ResponseTypesSupported: []string{"code"}, | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| tests := []struct { | ||
| name string | ||
| modify func(*OIDCDiscoveryDocument) | ||
| isOIDC bool | ||
| wantErr error | ||
| }{ | ||
| {"valid OAuth document", nil, false, nil}, | ||
| {"valid OIDC document", nil, true, nil}, | ||
| {"missing issuer", func(d *OIDCDiscoveryDocument) { d.Issuer = "" }, false, ErrMissingIssuer}, | ||
| {"missing authorization_endpoint", func(d *OIDCDiscoveryDocument) { d.AuthorizationEndpoint = "" }, false, ErrMissingAuthorizationEndpoint}, | ||
| {"missing token_endpoint", func(d *OIDCDiscoveryDocument) { d.TokenEndpoint = "" }, false, ErrMissingTokenEndpoint}, | ||
| {"missing jwks_uri for OIDC", func(d *OIDCDiscoveryDocument) { d.JWKSURI = "" }, true, ErrMissingJWKSURI}, | ||
| {"missing jwks_uri for OAuth is OK", func(d *OIDCDiscoveryDocument) { d.JWKSURI = "" }, false, nil}, | ||
| {"missing response_types_supported for OIDC", func(d *OIDCDiscoveryDocument) { d.ResponseTypesSupported = nil }, true, ErrMissingResponseTypesSupported}, | ||
| {"missing response_types_supported for OAuth is OK", func(d *OIDCDiscoveryDocument) { d.ResponseTypesSupported = nil }, false, nil}, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| t.Parallel() | ||
| doc := validDoc() | ||
| if tt.modify != nil { | ||
| tt.modify(&doc) | ||
| } | ||
| err := doc.Validate(tt.isOIDC) | ||
| if !errors.Is(err, tt.wantErr) { | ||
| t.Errorf("Validate() = %v, want %v", err, tt.wantErr) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestOIDCDiscoveryDocument_SupportsPKCE(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| tests := []struct { | ||
| name string | ||
| methods []string | ||
| want bool | ||
| }{ | ||
| {"nil slice", nil, false}, | ||
| {"empty slice", []string{}, false}, | ||
| {"only plain", []string{"plain"}, false}, | ||
| {"S256 present", []string{"S256"}, true}, | ||
| {"both plain and S256", []string{"plain", "S256"}, true}, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| t.Parallel() | ||
| doc := OIDCDiscoveryDocument{ | ||
| AuthorizationServerMetadata: AuthorizationServerMetadata{ | ||
| CodeChallengeMethodsSupported: tt.methods, | ||
| }, | ||
| } | ||
| if got := doc.SupportsPKCE(); got != tt.want { | ||
| t.Errorf("SupportsPKCE() = %v, want %v", got, tt.want) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestOIDCDiscoveryDocument_SupportsGrantType(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| tests := []struct { | ||
| name string | ||
| grants []string | ||
| grantType string | ||
| want bool | ||
| }{ | ||
| {"nil slice", nil, GrantTypeAuthorizationCode, false}, | ||
| {"empty slice", []string{}, GrantTypeAuthorizationCode, false}, | ||
| {"grant type present", []string{GrantTypeAuthorizationCode}, GrantTypeAuthorizationCode, true}, | ||
| {"grant type absent", []string{GrantTypeRefreshToken}, GrantTypeAuthorizationCode, false}, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| t.Parallel() | ||
| doc := OIDCDiscoveryDocument{ | ||
| AuthorizationServerMetadata: AuthorizationServerMetadata{ | ||
| GrantTypesSupported: tt.grants, | ||
| }, | ||
| } | ||
| if got := doc.SupportsGrantType(tt.grantType); got != tt.want { | ||
| t.Errorf("SupportsGrantType(%q) = %v, want %v", tt.grantType, got, tt.want) | ||
| } | ||
| }) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| // SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| // Package oauth provides shared RFC-defined types, constants, and validation utilities | ||
| // for OAuth 2.0 and OpenID Connect. It serves as a shared foundation for both OAuth | ||
| // clients and servers, including redirect URI validation per RFC 6749 and RFC 8252. | ||
| // | ||
| // # Discovery Documents | ||
| // | ||
| // The package provides types for OAuth 2.0 Authorization Server Metadata (RFC 8414) | ||
| // and OpenID Connect Discovery 1.0: | ||
| // | ||
| // doc := oauth.OIDCDiscoveryDocument{ | ||
| // AuthorizationServerMetadata: oauth.AuthorizationServerMetadata{ | ||
| // Issuer: "https://auth.example.com", | ||
| // AuthorizationEndpoint: "https://auth.example.com/authorize", | ||
| // TokenEndpoint: "https://auth.example.com/token", | ||
| // }, | ||
| // } | ||
| // if err := doc.Validate(true); err != nil { | ||
| // // Handle validation error | ||
| // } | ||
| // | ||
| // # Redirect URI Validation | ||
| // | ||
| // The package provides RFC-compliant redirect URI validation with configurable | ||
| // policies for security: | ||
| // | ||
| // // Strict policy: only https and http-loopback | ||
| // err := oauth.ValidateRedirectURI("https://example.com/callback", oauth.RedirectURIPolicyStrict) | ||
| // | ||
| // // Allow private-use schemes for native apps | ||
| // err := oauth.ValidateRedirectURI("myapp://callback", oauth.RedirectURIPolicyAllowPrivateSchemes) | ||
| // | ||
| // # Stability | ||
| // | ||
| // This package is Beta stability. The API may have minor changes before | ||
| // reaching stable status in v1.0.0. | ||
| package oauth |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.