From 71c8373eb45a30fd91c8e067eebce14eae802783 Mon Sep 17 00:00:00 2001 From: Maxim Lebedev Date: Mon, 6 May 2024 20:58:14 +0500 Subject: [PATCH] :truck: Moved code challenge method into separated package --- .../auth/delivery/http/auth_http_schema.go | 27 +- internal/auth/delivery/http/auth_http_test.go | 3 +- internal/auth/usecase.go | 3 +- internal/auth/usecase/auth_ucase.go | 3 +- .../client/repository/http/http_client.go | 35 +- internal/domain/challenge/method.go | 132 ++++++++ internal/domain/challenge/method_test.go | 160 +++++++++ internal/domain/code_challenge_method.go | 137 -------- internal/domain/code_challenge_method_test.go | 160 --------- internal/domain/metadata.go | 15 +- internal/domain/session.go | 19 +- .../metadata/repository/http/http_metadata.go | 37 +- .../repository/http/http_metadata_test.go | 3 +- internal/token/usecase/token_ucase.go | 3 +- internal/user/repository/http/http_user.go | 35 +- main.go | 13 +- web/template/authorize.qtpl | 5 +- web/template/authorize.qtpl.go | 319 +++++++++--------- 18 files changed, 559 insertions(+), 550 deletions(-) create mode 100644 internal/domain/challenge/method.go create mode 100644 internal/domain/challenge/method_test.go delete mode 100644 internal/domain/code_challenge_method.go delete mode 100644 internal/domain/code_challenge_method_test.go diff --git a/internal/auth/delivery/http/auth_http_schema.go b/internal/auth/delivery/http/auth_http_schema.go index 6f91b5b..aec3bd9 100644 --- a/internal/auth/delivery/http/auth_http_schema.go +++ b/internal/auth/delivery/http/auth_http_schema.go @@ -6,6 +6,7 @@ import ( "strings" "source.toby3d.me/toby3d/auth/internal/domain" + "source.toby3d.me/toby3d/auth/internal/domain/challenge" "source.toby3d.me/toby3d/auth/internal/domain/grant" "source.toby3d.me/toby3d/auth/internal/domain/response" "source.toby3d.me/toby3d/form" @@ -24,7 +25,7 @@ type ( Me domain.Me `form:"me"` // The hashing method used to calculate the code challenge. - CodeChallengeMethod domain.CodeChallengeMethod `form:"code_challenge_method,omitempty"` + CodeChallengeMethod challenge.Method `form:"code_challenge_method,omitempty"` // Indicates to the authorization server that an authorization // code should be returned as the response. @@ -48,16 +49,16 @@ type ( } AuthVerifyRequest struct { - ClientID domain.ClientID `form:"client_id"` - Me domain.Me `form:"me"` - RedirectURI domain.URL `form:"redirect_uri"` - CodeChallengeMethod domain.CodeChallengeMethod `form:"code_challenge_method,omitempty"` - ResponseType response.Type `form:"response_type"` - Authorize string `form:"authorize"` - CodeChallenge string `form:"code_challenge,omitempty"` - State string `form:"state"` - Provider string `form:"provider"` - Scope domain.Scopes `form:"scope[],omitempty"` + ClientID domain.ClientID `form:"client_id"` + Me domain.Me `form:"me"` + RedirectURI domain.URL `form:"redirect_uri"` + CodeChallengeMethod challenge.Method `form:"code_challenge_method,omitempty"` + ResponseType response.Type `form:"response_type"` + Authorize string `form:"authorize"` + CodeChallenge string `form:"code_challenge,omitempty"` + State string `form:"state"` + Provider string `form:"provider"` + Scope domain.Scopes `form:"scope[],omitempty"` } AuthExchangeRequest struct { @@ -97,7 +98,7 @@ func NewAuthAuthorizationRequest() *AuthAuthorizationRequest { return &AuthAuthorizationRequest{ ClientID: domain.ClientID{}, CodeChallenge: "", - CodeChallengeMethod: domain.CodeChallengeMethodUnd, + CodeChallengeMethod: challenge.Und, Me: domain.Me{}, RedirectURI: domain.URL{}, ResponseType: response.Und, @@ -131,7 +132,7 @@ func NewAuthVerifyRequest() *AuthVerifyRequest { Authorize: "", ClientID: domain.ClientID{}, CodeChallenge: "", - CodeChallengeMethod: domain.CodeChallengeMethodUnd, + CodeChallengeMethod: challenge.Und, Me: domain.Me{}, Provider: "", RedirectURI: domain.URL{}, diff --git a/internal/auth/delivery/http/auth_http_test.go b/internal/auth/delivery/http/auth_http_test.go index f1f530e..a40193e 100644 --- a/internal/auth/delivery/http/auth_http_test.go +++ b/internal/auth/delivery/http/auth_http_test.go @@ -19,6 +19,7 @@ import ( clientrepo "source.toby3d.me/toby3d/auth/internal/client/repository/memory" clientucase "source.toby3d.me/toby3d/auth/internal/client/usecase" "source.toby3d.me/toby3d/auth/internal/domain" + "source.toby3d.me/toby3d/auth/internal/domain/challenge" "source.toby3d.me/toby3d/auth/internal/domain/response" "source.toby3d.me/toby3d/auth/internal/profile" profilerepo "source.toby3d.me/toby3d/auth/internal/profile/repository/memory" @@ -66,7 +67,7 @@ func TestAuthorize(t *testing.T) { for key, val := range map[string]string{ "client_id": client.ID.String(), "code_challenge": "OfYAxt8zU2dAPDWQxTAUIteRzMsoj9QBdMIVEDOErUo", - "code_challenge_method": domain.CodeChallengeMethodS256.String(), + "code_challenge_method": challenge.S256.String(), "me": me.String(), "redirect_uri": client.RedirectURI[0].String(), "response_type": response.Code.String(), diff --git a/internal/auth/usecase.go b/internal/auth/usecase.go index 39f8677..ecd4e1a 100644 --- a/internal/auth/usecase.go +++ b/internal/auth/usecase.go @@ -5,6 +5,7 @@ import ( "net/url" "source.toby3d.me/toby3d/auth/internal/domain" + "source.toby3d.me/toby3d/auth/internal/domain/challenge" ) type ( @@ -12,7 +13,7 @@ type ( ClientID domain.ClientID Me domain.Me RedirectURI *url.URL - CodeChallengeMethod domain.CodeChallengeMethod + CodeChallengeMethod challenge.Method CodeChallenge string Scope domain.Scopes } diff --git a/internal/auth/usecase/auth_ucase.go b/internal/auth/usecase/auth_ucase.go index 3eed5f9..bc1dc54 100644 --- a/internal/auth/usecase/auth_ucase.go +++ b/internal/auth/usecase/auth_ucase.go @@ -6,6 +6,7 @@ import ( "source.toby3d.me/toby3d/auth/internal/auth" "source.toby3d.me/toby3d/auth/internal/domain" + "source.toby3d.me/toby3d/auth/internal/domain/challenge" "source.toby3d.me/toby3d/auth/internal/profile" "source.toby3d.me/toby3d/auth/internal/random" "source.toby3d.me/toby3d/auth/internal/session" @@ -76,7 +77,7 @@ func (uc *authUseCase) Exchange(ctx context.Context, opts auth.ExchangeOptions) } if session.CodeChallenge != "" && - session.CodeChallengeMethod != domain.CodeChallengeMethodUnd && + session.CodeChallengeMethod != challenge.Und && !session.CodeChallengeMethod.Validate(session.CodeChallenge, opts.CodeVerifier) { return nil, nil, auth.ErrMismatchPKCE } diff --git a/internal/client/repository/http/http_client.go b/internal/client/repository/http/http_client.go index 152b86e..f93913f 100644 --- a/internal/client/repository/http/http_client.go +++ b/internal/client/repository/http/http_client.go @@ -15,6 +15,7 @@ import ( "source.toby3d.me/toby3d/auth/internal/client" "source.toby3d.me/toby3d/auth/internal/common" "source.toby3d.me/toby3d/auth/internal/domain" + "source.toby3d.me/toby3d/auth/internal/domain/challenge" "source.toby3d.me/toby3d/auth/internal/domain/grant" "source.toby3d.me/toby3d/auth/internal/domain/response" ) @@ -22,23 +23,23 @@ import ( type ( //nolint:tagliatelle,lll Response struct { - TicketEndpoint domain.URL `json:"ticket_endpoint"` - AuthorizationEndpoint domain.URL `json:"authorization_endpoint"` - IntrospectionEndpoint domain.URL `json:"introspection_endpoint"` - RevocationEndpoint domain.URL `json:"revocation_endpoint,omitempty"` - ServiceDocumentation domain.URL `json:"service_documentation,omitempty"` - TokenEndpoint domain.URL `json:"token_endpoint"` - UserinfoEndpoint domain.URL `json:"userinfo_endpoint,omitempty"` - Microsub domain.URL `json:"microsub"` - Issuer domain.URL `json:"issuer"` - Micropub domain.URL `json:"micropub"` - GrantTypesSupported []grant.Type `json:"grant_types_supported,omitempty"` - IntrospectionEndpointAuthMethodsSupported []string `json:"introspection_endpoint_auth_methods_supported,omitempty"` - RevocationEndpointAuthMethodsSupported []string `json:"revocation_endpoint_auth_methods_supported,omitempty"` - ScopesSupported []domain.Scope `json:"scopes_supported,omitempty"` - ResponseTypesSupported []response.Type `json:"response_types_supported,omitempty"` - CodeChallengeMethodsSupported []domain.CodeChallengeMethod `json:"code_challenge_methods_supported"` - AuthorizationResponseIssParameterSupported bool `json:"authorization_response_iss_parameter_supported,omitempty"` + TicketEndpoint domain.URL `json:"ticket_endpoint"` + AuthorizationEndpoint domain.URL `json:"authorization_endpoint"` + IntrospectionEndpoint domain.URL `json:"introspection_endpoint"` + RevocationEndpoint domain.URL `json:"revocation_endpoint,omitempty"` + ServiceDocumentation domain.URL `json:"service_documentation,omitempty"` + TokenEndpoint domain.URL `json:"token_endpoint"` + UserinfoEndpoint domain.URL `json:"userinfo_endpoint,omitempty"` + Microsub domain.URL `json:"microsub"` + Issuer domain.URL `json:"issuer"` + Micropub domain.URL `json:"micropub"` + GrantTypesSupported []grant.Type `json:"grant_types_supported,omitempty"` + IntrospectionEndpointAuthMethodsSupported []string `json:"introspection_endpoint_auth_methods_supported,omitempty"` + RevocationEndpointAuthMethodsSupported []string `json:"revocation_endpoint_auth_methods_supported,omitempty"` + ScopesSupported []domain.Scope `json:"scopes_supported,omitempty"` + ResponseTypesSupported []response.Type `json:"response_types_supported,omitempty"` + CodeChallengeMethodsSupported []challenge.Method `json:"code_challenge_methods_supported"` + AuthorizationResponseIssParameterSupported bool `json:"authorization_response_iss_parameter_supported,omitempty"` } httpClientRepository struct { diff --git a/internal/domain/challenge/method.go b/internal/domain/challenge/method.go new file mode 100644 index 0000000..46c598e --- /dev/null +++ b/internal/domain/challenge/method.go @@ -0,0 +1,132 @@ +package challenge + +//nolint:gosec // support old clients +import ( + "crypto/md5" + "crypto/sha1" + "crypto/sha256" + "crypto/sha512" + "encoding/base64" + "errors" + "fmt" + "hash" + "strconv" + "strings" + + "source.toby3d.me/toby3d/auth/internal/common" +) + +// Method represent a PKCE challenge method for validate verifier. +// +// NOTE(toby3d): Encapsulate enums in structs for extra compile-time safety: +// https://threedots.tech/post/safer-enums-in-go/#struct-based-enums +type Method struct { + codeChallengeMethod string +} + +//nolint:gochecknoglobals // structs cannot be constants +var ( + Und = Method{} // "und" + PLAIN = Method{"plain"} // "plain" + MD5 = Method{"md5"} // "md5" + S1 = Method{"s1"} // "s1" + S256 = Method{"s256"} // "s256" + S512 = Method{"s512"} // "s512" +) + +var ErrCodeChallengeMethodUnknown error = errors.New("unknown code_challenge_method") + +//nolint:gochecknoglobals // maps cannot be constants +var uidsMethods = map[string]Method{ + MD5.codeChallengeMethod: MD5, + PLAIN.codeChallengeMethod: PLAIN, + S1.codeChallengeMethod: S1, + S256.codeChallengeMethod: S256, + S512.codeChallengeMethod: S512, +} + +// ParseMethod parse string identifier of code challenge method into struct enum. +func ParseMethod(uid string) (Method, error) { + if method, ok := uidsMethods[uid]; ok { + return method, nil + } + + return Und, fmt.Errorf("%w: %s", ErrCodeChallengeMethodUnknown, uid) +} + +// UnmarshalForm implements custom unmarshler for form values. +func (m *Method) UnmarshalForm(v []byte) error { + parsed, err := ParseMethod(strings.ToLower(string(v))) + if err != nil { + return fmt.Errorf("CodeChallengeMethod: UnmarshalForm: %w", err) + } + + *m = parsed + + return nil +} + +// UnmarshalJSON implements custom unmarshler for JSON. +func (m *Method) UnmarshalJSON(v []byte) error { + unquoted, err := strconv.Unquote(string(v)) + if err != nil { + return fmt.Errorf("CodeChallengeMethod: UnmarshalJSON: cannot unquote value '%s': %w", string(v), err) + } + + parsed, err := ParseMethod(strings.ToLower(unquoted)) + if err != nil && !errors.Is(err, ErrCodeChallengeMethodUnknown) { + return fmt.Errorf("CodeChallengeMethod: UnmarshalJSON: cannot parse '%s' value: %w", unquoted, err) + } + + *m = parsed + + return nil +} + +func (m Method) MarshalJSON() ([]byte, error) { + if m == Und { + return nil, nil + } + + return []byte(strconv.Quote(m.String())), nil +} + +// String returns string representation of code challenge method. +func (m Method) String() string { + if m == Und { + return common.Und + } + + return strings.ToUpper(m.codeChallengeMethod) +} + +func (m Method) GoString() string { + return "challenge.Method(" + m.String() + ")" +} + +// Validate checks for a match to the verifier with the hashed version of the +// challenge via the chosen method. +func (m Method) Validate(codeChallenge, verifier string) bool { + var h hash.Hash + + switch m { + default: + return false + case PLAIN: + return codeChallenge == verifier + case MD5: + h = md5.New() + case S1: + h = sha1.New() + case S256: + h = sha256.New() + case S512: + h = sha512.New() + } + + if _, err := h.Write([]byte(verifier)); err != nil { + return false + } + + return codeChallenge == base64.RawURLEncoding.EncodeToString(h.Sum(nil)) +} diff --git a/internal/domain/challenge/method_test.go b/internal/domain/challenge/method_test.go new file mode 100644 index 0000000..58c0e70 --- /dev/null +++ b/internal/domain/challenge/method_test.go @@ -0,0 +1,160 @@ +package challenge_test + +//nolint:gosec // support old clients + +import ( + "crypto/md5" + "crypto/sha1" + "crypto/sha256" + "crypto/sha512" + "encoding/base64" + "hash" + "testing" + + "github.com/brianvoe/gofakeit/v6" + + "source.toby3d.me/toby3d/auth/internal/domain/challenge" + "source.toby3d.me/toby3d/auth/internal/random" +) + +func TestParseMethod(t *testing.T) { + t.Parallel() + + for name, tc := range map[string]struct { + input string + expect challenge.Method + }{ + "PLAIN": {input: "plain", expect: challenge.PLAIN}, + "MD5": {input: "md5", expect: challenge.MD5}, + "S1": {input: "s1", expect: challenge.S1}, + "S256": {input: "s256", expect: challenge.S256}, + "S512": {input: "s512", expect: challenge.S512}, + } { + t.Run(name, func(t *testing.T) { + t.Parallel() + + actual, err := challenge.ParseMethod(tc.input) + if err != nil { + t.Fatal(err) + } + + if actual != tc.expect { + t.Errorf("ParseMethod(%s) = %v, want %v", tc.input, actual, tc.expect) + } + }) + } +} + +func TestMethod_UnmarshalForm(t *testing.T) { + t.Parallel() + + input := []byte("s256") + actual := challenge.Und + + if err := actual.UnmarshalForm(input); err != nil { + t.Fatal(err) + } + + if actual != challenge.S256 { + t.Errorf("UnmarshalForm(%s) = %v, want %v", input, actual, challenge.S256) + } +} + +func TestMethod_UnmarshalJSON(t *testing.T) { + t.Parallel() + + input := []byte(`"S256"`) + actual := challenge.Und + + if err := actual.UnmarshalJSON(input); err != nil { + t.Fatal(err) + } + + if actual != challenge.S256 { + t.Errorf("UnmarshalJSON(%s) = %v, want %v", input, actual, challenge.S256) + } +} + +func TestMethod_String(t *testing.T) { + t.Parallel() + + for name, tc := range map[string]struct { + input challenge.Method + expect string + }{ + "plain": {input: challenge.PLAIN, expect: "PLAIN"}, + "md5": {input: challenge.MD5, expect: "MD5"}, + "s1": {input: challenge.S1, expect: "S1"}, + "s256": {input: challenge.S256, expect: "S256"}, + "s512": {input: challenge.S512, expect: "S512"}, + } { + t.Run(name, func(t *testing.T) { + t.Parallel() + + if actual := tc.input.String(); actual != tc.expect { + t.Errorf("String() = %v, want %v", actual, tc.expect) + } + }) + } +} + +//nolint:gosec // support old clients +func TestMethod_Validate(t *testing.T) { + t.Parallel() + + verifier, err := random.String(uint8(gofakeit.Number(43, 128))) + if err != nil { + t.Fatal(err) + } + + for name, tc := range map[string]struct { + hash hash.Hash + input challenge.Method + ok bool + }{ + "invalid": {input: challenge.S256, hash: md5.New(), ok: true}, + "MD5": {input: challenge.MD5, hash: md5.New(), ok: false}, + "plain": {input: challenge.PLAIN, hash: nil, ok: false}, + "S1": {input: challenge.S1, hash: sha1.New(), ok: false}, + "S256": {input: challenge.S256, hash: sha256.New(), ok: false}, + "S512": {input: challenge.S512, hash: sha512.New(), ok: false}, + "Und": {input: challenge.Und, hash: nil, ok: true}, + } { + t.Run(name, func(t *testing.T) { + t.Parallel() + + var codeChallenge string + + switch tc.input { + case challenge.Und, challenge.PLAIN: + codeChallenge = verifier + default: + hash := tc.hash + hash.Reset() + + if _, err := hash.Write([]byte(verifier)); err != nil { + t.Error(err) + } + + codeChallenge = base64.RawURLEncoding.EncodeToString(hash.Sum(nil)) + } + + if actual := tc.input.Validate(codeChallenge, verifier); actual != !tc.ok { + t.Errorf("Validate(%s, %s) = %t, want %t", codeChallenge, verifier, actual, tc.ok) + } + }) + } +} + +func TestMethod_Validate_IndieAuth(t *testing.T) { + t.Parallel() + + if ok := challenge.S256.Validate( + "ALiMNf5FvF_LIWLhSkd9tjPKh3PEmai2OrdDBzrVZ3M", + "6f535c952339f0670311b4bbec5c41c00805e83291fc7eb15ca4963f82a4d57595787dcc6ee90571fb7789cbd521fe0178ed", + ); !ok { + t.Errorf("Validate(%s, %s) = %t, want %t", "ALiMNf5FvF_LIWLhSkd9tjPKh3PEmai2OrdDBzrVZ3M", + "6f535c952339f0670311b4bbec5c41c00805e83291fc7eb15ca4963f82a4d57595787dcc6ee90571fb7789cbd521"+ + "fe0178ed", ok, true) + } +} diff --git a/internal/domain/code_challenge_method.go b/internal/domain/code_challenge_method.go deleted file mode 100644 index 6566ec1..0000000 --- a/internal/domain/code_challenge_method.go +++ /dev/null @@ -1,137 +0,0 @@ -package domain - -//nolint:gosec // support old clients -import ( - "crypto/md5" - "crypto/sha1" - "crypto/sha256" - "crypto/sha512" - "encoding/base64" - "errors" - "fmt" - "hash" - "strconv" - "strings" - - "source.toby3d.me/toby3d/auth/internal/common" -) - -// CodeChallengeMethod represent a PKCE challenge method for validate verifier. -// -// NOTE(toby3d): Encapsulate enums in structs for extra compile-time safety: -// https://threedots.tech/post/safer-enums-in-go/#struct-based-enums -type CodeChallengeMethod struct { - codeChallengeMethod string -} - -//nolint:gochecknoglobals // structs cannot be constants -var ( - CodeChallengeMethodUnd = CodeChallengeMethod{codeChallengeMethod: ""} // "und" - CodeChallengeMethodPLAIN = CodeChallengeMethod{codeChallengeMethod: "plain"} // "plain" - CodeChallengeMethodMD5 = CodeChallengeMethod{codeChallengeMethod: "md5"} // "md5" - CodeChallengeMethodS1 = CodeChallengeMethod{codeChallengeMethod: "s1"} // "s1" - CodeChallengeMethodS256 = CodeChallengeMethod{codeChallengeMethod: "s256"} // "s256" - CodeChallengeMethodS512 = CodeChallengeMethod{codeChallengeMethod: "s512"} // "s512" -) - -var ErrCodeChallengeMethodUnknown error = NewError( - ErrorCodeInvalidRequest, - "unknown code_challenge_method", - "https://indieauth.net/source/#authorization-request", -) - -//nolint:gochecknoglobals // maps cannot be constants -var uidsMethods = map[string]CodeChallengeMethod{ - CodeChallengeMethodMD5.codeChallengeMethod: CodeChallengeMethodMD5, - CodeChallengeMethodPLAIN.codeChallengeMethod: CodeChallengeMethodPLAIN, - CodeChallengeMethodS1.codeChallengeMethod: CodeChallengeMethodS1, - CodeChallengeMethodS256.codeChallengeMethod: CodeChallengeMethodS256, - CodeChallengeMethodS512.codeChallengeMethod: CodeChallengeMethodS512, -} - -// ParseCodeChallengeMethod parse string identifier of code challenge method -// into struct enum. -func ParseCodeChallengeMethod(uid string) (CodeChallengeMethod, error) { - if method, ok := uidsMethods[uid]; ok { - return method, nil - } - - return CodeChallengeMethodUnd, fmt.Errorf("%w: %s", ErrCodeChallengeMethodUnknown, uid) -} - -// UnmarshalForm implements custom unmarshler for form values. -func (ccm *CodeChallengeMethod) UnmarshalForm(v []byte) error { - parsed, err := ParseCodeChallengeMethod(strings.ToLower(string(v))) - if err != nil { - return fmt.Errorf("CodeChallengeMethod: UnmarshalForm: %w", err) - } - - *ccm = parsed - - return nil -} - -// UnmarshalJSON implements custom unmarshler for JSON. -func (ccm *CodeChallengeMethod) UnmarshalJSON(v []byte) error { - unquoted, err := strconv.Unquote(string(v)) - if err != nil { - return fmt.Errorf("CodeChallengeMethod: UnmarshalJSON: cannot unquote value '%s': %w", string(v), err) - } - - parsed, err := ParseCodeChallengeMethod(strings.ToLower(unquoted)) - if err != nil && !errors.Is(err, ErrCodeChallengeMethodUnknown) { - return fmt.Errorf("CodeChallengeMethod: UnmarshalJSON: cannot parse '%s' value: %w", unquoted, err) - } - - *ccm = parsed - - return nil -} - -func (ccm CodeChallengeMethod) MarshalJSON() ([]byte, error) { - if ccm == CodeChallengeMethodUnd { - return nil, nil - } - - return []byte(strconv.Quote(ccm.String())), nil -} - -// String returns string representation of code challenge method. -func (ccm CodeChallengeMethod) String() string { - if ccm == CodeChallengeMethodUnd { - return common.Und - } - - return strings.ToUpper(ccm.codeChallengeMethod) -} - -func (ccm CodeChallengeMethod) GoString() string { - return "domain.CodeChallengeMethod(" + ccm.String() + ")" -} - -// Validate checks for a match to the verifier with the hashed version of the -// challenge via the chosen method. -func (ccm CodeChallengeMethod) Validate(codeChallenge, verifier string) bool { - var h hash.Hash - - switch ccm { - default: - return false - case CodeChallengeMethodPLAIN: - return codeChallenge == verifier - case CodeChallengeMethodMD5: - h = md5.New() - case CodeChallengeMethodS1: - h = sha1.New() - case CodeChallengeMethodS256: - h = sha256.New() - case CodeChallengeMethodS512: - h = sha512.New() - } - - if _, err := h.Write([]byte(verifier)); err != nil { - return false - } - - return codeChallenge == base64.RawURLEncoding.EncodeToString(h.Sum(nil)) -} diff --git a/internal/domain/code_challenge_method_test.go b/internal/domain/code_challenge_method_test.go deleted file mode 100644 index f232160..0000000 --- a/internal/domain/code_challenge_method_test.go +++ /dev/null @@ -1,160 +0,0 @@ -package domain_test - -//nolint:gosec // support old clients - -import ( - "crypto/md5" - "crypto/sha1" - "crypto/sha256" - "crypto/sha512" - "encoding/base64" - "hash" - "testing" - - "github.com/brianvoe/gofakeit/v6" - - "source.toby3d.me/toby3d/auth/internal/domain" - "source.toby3d.me/toby3d/auth/internal/random" -) - -func TestParseCodeChallengeMethod(t *testing.T) { - t.Parallel() - - for name, tc := range map[string]struct { - input string - expect domain.CodeChallengeMethod - }{ - "PLAIN": {input: "plain", expect: domain.CodeChallengeMethodPLAIN}, - "MD5": {input: "md5", expect: domain.CodeChallengeMethodMD5}, - "S1": {input: "s1", expect: domain.CodeChallengeMethodS1}, - "S256": {input: "s256", expect: domain.CodeChallengeMethodS256}, - "S512": {input: "s512", expect: domain.CodeChallengeMethodS512}, - } { - t.Run(name, func(t *testing.T) { - t.Parallel() - - actual, err := domain.ParseCodeChallengeMethod(tc.input) - if err != nil { - t.Fatal(err) - } - - if actual != tc.expect { - t.Errorf("ParseCodeChallengeMethod(%s) = %v, want %v", tc.input, actual, tc.expect) - } - }) - } -} - -func TestCodeChallengeMethod_UnmarshalForm(t *testing.T) { - t.Parallel() - - input := []byte("s256") - actual := domain.CodeChallengeMethodUnd - - if err := actual.UnmarshalForm(input); err != nil { - t.Fatal(err) - } - - if actual != domain.CodeChallengeMethodS256 { - t.Errorf("UnmarshalForm(%s) = %v, want %v", input, actual, domain.CodeChallengeMethodS256) - } -} - -func TestCodeChallengeMethod_UnmarshalJSON(t *testing.T) { - t.Parallel() - - input := []byte(`"S256"`) - actual := domain.CodeChallengeMethodUnd - - if err := actual.UnmarshalJSON(input); err != nil { - t.Fatal(err) - } - - if actual != domain.CodeChallengeMethodS256 { - t.Errorf("UnmarshalJSON(%s) = %v, want %v", input, actual, domain.CodeChallengeMethodS256) - } -} - -func TestCodeChallengeMethod_String(t *testing.T) { - t.Parallel() - - for name, tc := range map[string]struct { - input domain.CodeChallengeMethod - expect string - }{ - "plain": {input: domain.CodeChallengeMethodPLAIN, expect: "PLAIN"}, - "md5": {input: domain.CodeChallengeMethodMD5, expect: "MD5"}, - "s1": {input: domain.CodeChallengeMethodS1, expect: "S1"}, - "s256": {input: domain.CodeChallengeMethodS256, expect: "S256"}, - "s512": {input: domain.CodeChallengeMethodS512, expect: "S512"}, - } { - t.Run(name, func(t *testing.T) { - t.Parallel() - - if actual := tc.input.String(); actual != tc.expect { - t.Errorf("String() = %v, want %v", actual, tc.expect) - } - }) - } -} - -//nolint:gosec // support old clients -func TestCodeChallengeMethod_Validate(t *testing.T) { - t.Parallel() - - verifier, err := random.String(uint8(gofakeit.Number(43, 128))) - if err != nil { - t.Fatal(err) - } - - for name, tc := range map[string]struct { - hash hash.Hash - input domain.CodeChallengeMethod - ok bool - }{ - "invalid": {input: domain.CodeChallengeMethodS256, hash: md5.New(), ok: true}, - "MD5": {input: domain.CodeChallengeMethodMD5, hash: md5.New(), ok: false}, - "plain": {input: domain.CodeChallengeMethodPLAIN, hash: nil, ok: false}, - "S1": {input: domain.CodeChallengeMethodS1, hash: sha1.New(), ok: false}, - "S256": {input: domain.CodeChallengeMethodS256, hash: sha256.New(), ok: false}, - "S512": {input: domain.CodeChallengeMethodS512, hash: sha512.New(), ok: false}, - "Und": {input: domain.CodeChallengeMethodUnd, hash: nil, ok: true}, - } { - t.Run(name, func(t *testing.T) { - t.Parallel() - - var codeChallenge string - - switch tc.input { - case domain.CodeChallengeMethodUnd, domain.CodeChallengeMethodPLAIN: - codeChallenge = verifier - default: - hash := tc.hash - hash.Reset() - - if _, err := hash.Write([]byte(verifier)); err != nil { - t.Error(err) - } - - codeChallenge = base64.RawURLEncoding.EncodeToString(hash.Sum(nil)) - } - - if actual := tc.input.Validate(codeChallenge, verifier); actual != !tc.ok { - t.Errorf("Validate(%s, %s) = %t, want %t", codeChallenge, verifier, actual, tc.ok) - } - }) - } -} - -func TestCodeChallengeMethod_Validate_IndieAuth(t *testing.T) { - t.Parallel() - - if ok := domain.CodeChallengeMethodS256.Validate( - "ALiMNf5FvF_LIWLhSkd9tjPKh3PEmai2OrdDBzrVZ3M", - "6f535c952339f0670311b4bbec5c41c00805e83291fc7eb15ca4963f82a4d57595787dcc6ee90571fb7789cbd521fe0178ed", - ); !ok { - t.Errorf("Validate(%s, %s) = %t, want %t", "ALiMNf5FvF_LIWLhSkd9tjPKh3PEmai2OrdDBzrVZ3M", - "6f535c952339f0670311b4bbec5c41c00805e83291fc7eb15ca4963f82a4d57595787dcc6ee90571fb7789cbd521"+ - "fe0178ed", ok, true) - } -} diff --git a/internal/domain/metadata.go b/internal/domain/metadata.go index 3555fac..0bd460a 100644 --- a/internal/domain/metadata.go +++ b/internal/domain/metadata.go @@ -4,6 +4,7 @@ import ( "net/url" "testing" + "source.toby3d.me/toby3d/auth/internal/domain/challenge" "source.toby3d.me/toby3d/auth/internal/domain/grant" "source.toby3d.me/toby3d/auth/internal/domain/response" ) @@ -64,7 +65,7 @@ type Metadata struct { // JSON array containing the methods supported for PKCE. This parameter // differs from RFC8414 in that it is not optional as PKCE is REQUIRED. - CodeChallengeMethodsSupported []CodeChallengeMethod + CodeChallengeMethodsSupported []challenge.Method // List of client authentication methods supported by this introspection endpoint. IntrospectionEndpointAuthMethodsSupported []string // ["Bearer"] @@ -119,12 +120,12 @@ func TestMetadata(tb testing.TB) *Metadata { grant.AuthorizationCode, grant.Ticket, }, - CodeChallengeMethodsSupported: []CodeChallengeMethod{ - CodeChallengeMethodMD5, - CodeChallengeMethodPLAIN, - CodeChallengeMethodS1, - CodeChallengeMethodS256, - CodeChallengeMethodS512, + CodeChallengeMethodsSupported: []challenge.Method{ + challenge.MD5, + challenge.PLAIN, + challenge.S1, + challenge.S256, + challenge.S512, }, IntrospectionEndpointAuthMethodsSupported: []string{"Bearer"}, RevocationEndpointAuthMethodsSupported: []string{"none"}, diff --git a/internal/domain/session.go b/internal/domain/session.go index fbf34c5..7dd6ccd 100644 --- a/internal/domain/session.go +++ b/internal/domain/session.go @@ -4,19 +4,20 @@ import ( "net/url" "testing" + "source.toby3d.me/toby3d/auth/internal/domain/challenge" "source.toby3d.me/toby3d/auth/internal/random" ) //nolint:tagliatelle type Session struct { - ClientID ClientID `json:"client_id"` - RedirectURI *url.URL `json:"redirect_uri"` - Me Me `json:"me"` - Profile *Profile `json:"profile,omitempty"` - CodeChallengeMethod CodeChallengeMethod `json:"code_challenge_method,omitempty"` - CodeChallenge string `json:"code_challenge,omitempty"` - Code string `json:"-"` - Scope Scopes `json:"scope"` + ClientID ClientID `json:"client_id"` + RedirectURI *url.URL `json:"redirect_uri"` + Me Me `json:"me"` + Profile *Profile `json:"profile,omitempty"` + CodeChallengeMethod challenge.Method `json:"code_challenge_method,omitempty"` + CodeChallenge string `json:"code_challenge,omitempty"` + Code string `json:"-"` + Scope Scopes `json:"scope"` } // TestSession returns valid random generated session for tests. @@ -34,7 +35,7 @@ func TestSession(tb testing.TB) *Session { ClientID: *TestClientID(tb), Code: code, CodeChallenge: "hackme", - CodeChallengeMethod: CodeChallengeMethodPLAIN, + CodeChallengeMethod: challenge.PLAIN, Profile: TestProfile(tb), Me: *TestMe(tb, "https://user.example.net/"), RedirectURI: &url.URL{Scheme: "https", Host: "example.com", Path: "/callback"}, diff --git a/internal/metadata/repository/http/http_metadata.go b/internal/metadata/repository/http/http_metadata.go index 10632f9..7afab7d 100644 --- a/internal/metadata/repository/http/http_metadata.go +++ b/internal/metadata/repository/http/http_metadata.go @@ -12,6 +12,7 @@ import ( "source.toby3d.me/toby3d/auth/internal/common" "source.toby3d.me/toby3d/auth/internal/domain" + "source.toby3d.me/toby3d/auth/internal/domain/challenge" "source.toby3d.me/toby3d/auth/internal/domain/grant" "source.toby3d.me/toby3d/auth/internal/domain/response" "source.toby3d.me/toby3d/auth/internal/metadata" @@ -20,23 +21,23 @@ import ( type ( //nolint:tagliatelle,lll Response struct { - TicketEndpoint domain.URL `json:"ticket_endpoint"` - AuthorizationEndpoint domain.URL `json:"authorization_endpoint"` - IntrospectionEndpoint domain.URL `json:"introspection_endpoint"` - RevocationEndpoint domain.URL `json:"revocation_endpoint,omitempty"` - ServiceDocumentation domain.URL `json:"service_documentation,omitempty"` - TokenEndpoint domain.URL `json:"token_endpoint"` - UserinfoEndpoint domain.URL `json:"userinfo_endpoint,omitempty"` - Microsub domain.URL `json:"microsub"` - Issuer domain.URL `json:"issuer"` - Micropub domain.URL `json:"micropub"` - GrantTypesSupported []grant.Type `json:"grant_types_supported,omitempty"` - IntrospectionEndpointAuthMethodsSupported []string `json:"introspection_endpoint_auth_methods_supported,omitempty"` - RevocationEndpointAuthMethodsSupported []string `json:"revocation_endpoint_auth_methods_supported,omitempty"` - ScopesSupported []domain.Scope `json:"scopes_supported,omitempty"` - ResponseTypesSupported []response.Type `json:"response_types_supported,omitempty"` - CodeChallengeMethodsSupported []domain.CodeChallengeMethod `json:"code_challenge_methods_supported"` - AuthorizationResponseIssParameterSupported bool `json:"authorization_response_iss_parameter_supported,omitempty"` + TicketEndpoint domain.URL `json:"ticket_endpoint"` + AuthorizationEndpoint domain.URL `json:"authorization_endpoint"` + IntrospectionEndpoint domain.URL `json:"introspection_endpoint"` + RevocationEndpoint domain.URL `json:"revocation_endpoint,omitempty"` + ServiceDocumentation domain.URL `json:"service_documentation,omitempty"` + TokenEndpoint domain.URL `json:"token_endpoint"` + UserinfoEndpoint domain.URL `json:"userinfo_endpoint,omitempty"` + Microsub domain.URL `json:"microsub"` + Issuer domain.URL `json:"issuer"` + Micropub domain.URL `json:"micropub"` + GrantTypesSupported []grant.Type `json:"grant_types_supported,omitempty"` + IntrospectionEndpointAuthMethodsSupported []string `json:"introspection_endpoint_auth_methods_supported,omitempty"` + RevocationEndpointAuthMethodsSupported []string `json:"revocation_endpoint_auth_methods_supported,omitempty"` + ScopesSupported []domain.Scope `json:"scopes_supported,omitempty"` + ResponseTypesSupported []response.Type `json:"response_types_supported,omitempty"` + CodeChallengeMethodsSupported []challenge.Method `json:"code_challenge_methods_supported"` + AuthorizationResponseIssParameterSupported bool `json:"authorization_response_iss_parameter_supported,omitempty"` } httpMetadataRepository struct { @@ -119,7 +120,7 @@ func populateBuffer(dst map[string][]string, rel, u string) { func NewResponse() *Response { return &Response{ - CodeChallengeMethodsSupported: make([]domain.CodeChallengeMethod, 0), + CodeChallengeMethodsSupported: make([]challenge.Method, 0), GrantTypesSupported: make([]grant.Type, 0), ResponseTypesSupported: make([]response.Type, 0), ScopesSupported: make([]domain.Scope, 0), diff --git a/internal/metadata/repository/http/http_metadata_test.go b/internal/metadata/repository/http/http_metadata_test.go index 5dd2964..d5070b8 100644 --- a/internal/metadata/repository/http/http_metadata_test.go +++ b/internal/metadata/repository/http/http_metadata_test.go @@ -14,6 +14,7 @@ import ( "source.toby3d.me/toby3d/auth/internal/common" "source.toby3d.me/toby3d/auth/internal/domain" + "source.toby3d.me/toby3d/auth/internal/domain/challenge" "source.toby3d.me/toby3d/auth/internal/domain/grant" "source.toby3d.me/toby3d/auth/internal/domain/response" repository "source.toby3d.me/toby3d/auth/internal/metadata/repository/http" @@ -121,7 +122,7 @@ func TestGet(t *testing.T) { if diff := cmp.Diff(tc.out, out, cmp.AllowUnexported( domain.ClientID{}, - domain.CodeChallengeMethod{}, + challenge.Und, grant.Und, response.Und, domain.Scope{}, diff --git a/internal/token/usecase/token_ucase.go b/internal/token/usecase/token_ucase.go index ce72e6c..7f36170 100644 --- a/internal/token/usecase/token_ucase.go +++ b/internal/token/usecase/token_ucase.go @@ -9,6 +9,7 @@ import ( "github.com/lestrrat-go/jwx/v2/jwt" "source.toby3d.me/toby3d/auth/internal/domain" + "source.toby3d.me/toby3d/auth/internal/domain/challenge" "source.toby3d.me/toby3d/auth/internal/profile" "source.toby3d.me/toby3d/auth/internal/session" "source.toby3d.me/toby3d/auth/internal/token" @@ -58,7 +59,7 @@ func (uc *tokenUseCase) Exchange(ctx context.Context, opts token.ExchangeOptions return nil, nil, token.ErrMismatchRedirectURI } - if session.CodeChallenge != "" && session.CodeChallengeMethod != domain.CodeChallengeMethodUnd && + if session.CodeChallenge != "" && session.CodeChallengeMethod != challenge.Und && !session.CodeChallengeMethod.Validate(session.CodeChallenge, opts.CodeVerifier) { return nil, nil, token.ErrMismatchPKCE } diff --git a/internal/user/repository/http/http_user.go b/internal/user/repository/http/http_user.go index ae80562..336296f 100644 --- a/internal/user/repository/http/http_user.go +++ b/internal/user/repository/http/http_user.go @@ -15,6 +15,7 @@ import ( "source.toby3d.me/toby3d/auth/internal/common" "source.toby3d.me/toby3d/auth/internal/domain" + "source.toby3d.me/toby3d/auth/internal/domain/challenge" "source.toby3d.me/toby3d/auth/internal/domain/grant" "source.toby3d.me/toby3d/auth/internal/domain/response" "source.toby3d.me/toby3d/auth/internal/user" @@ -23,23 +24,23 @@ import ( type ( //nolint:tagliatelle,lll MetadataResponse struct { - TicketEndpoint domain.URL `json:"ticket_endpoint"` - AuthorizationEndpoint domain.URL `json:"authorization_endpoint"` - IntrospectionEndpoint domain.URL `json:"introspection_endpoint"` - RevocationEndpoint domain.URL `json:"revocation_endpoint,omitempty"` - ServiceDocumentation domain.URL `json:"service_documentation,omitempty"` - TokenEndpoint domain.URL `json:"token_endpoint"` - UserinfoEndpoint domain.URL `json:"userinfo_endpoint,omitempty"` - Microsub domain.URL `json:"microsub"` - Issuer domain.URL `json:"issuer"` - Micropub domain.URL `json:"micropub"` - GrantTypesSupported []grant.Type `json:"grant_types_supported,omitempty"` - IntrospectionEndpointAuthMethodsSupported []string `json:"introspection_endpoint_auth_methods_supported,omitempty"` - RevocationEndpointAuthMethodsSupported []string `json:"revocation_endpoint_auth_methods_supported,omitempty"` - ScopesSupported []domain.Scope `json:"scopes_supported,omitempty"` - ResponseTypesSupported []response.Type `json:"response_types_supported,omitempty"` - CodeChallengeMethodsSupported []domain.CodeChallengeMethod `json:"code_challenge_methods_supported"` - AuthorizationResponseIssParameterSupported bool `json:"authorization_response_iss_parameter_supported,omitempty"` + TicketEndpoint domain.URL `json:"ticket_endpoint"` + AuthorizationEndpoint domain.URL `json:"authorization_endpoint"` + IntrospectionEndpoint domain.URL `json:"introspection_endpoint"` + RevocationEndpoint domain.URL `json:"revocation_endpoint,omitempty"` + ServiceDocumentation domain.URL `json:"service_documentation,omitempty"` + TokenEndpoint domain.URL `json:"token_endpoint"` + UserinfoEndpoint domain.URL `json:"userinfo_endpoint,omitempty"` + Microsub domain.URL `json:"microsub"` + Issuer domain.URL `json:"issuer"` + Micropub domain.URL `json:"micropub"` + GrantTypesSupported []grant.Type `json:"grant_types_supported,omitempty"` + IntrospectionEndpointAuthMethodsSupported []string `json:"introspection_endpoint_auth_methods_supported,omitempty"` + RevocationEndpointAuthMethodsSupported []string `json:"revocation_endpoint_auth_methods_supported,omitempty"` + ScopesSupported []domain.Scope `json:"scopes_supported,omitempty"` + ResponseTypesSupported []response.Type `json:"response_types_supported,omitempty"` + CodeChallengeMethodsSupported []challenge.Method `json:"code_challenge_methods_supported"` + AuthorizationResponseIssParameterSupported bool `json:"authorization_response_iss_parameter_supported,omitempty"` } httpUserRepository struct { diff --git a/main.go b/main.go index c58aade..99c9920 100644 --- a/main.go +++ b/main.go @@ -37,6 +37,7 @@ import ( clienthttprepo "source.toby3d.me/toby3d/auth/internal/client/repository/http" clientucase "source.toby3d.me/toby3d/auth/internal/client/usecase" "source.toby3d.me/toby3d/auth/internal/domain" + "source.toby3d.me/toby3d/auth/internal/domain/challenge" "source.toby3d.me/toby3d/auth/internal/domain/grant" "source.toby3d.me/toby3d/auth/internal/domain/response" healthhttpdelivery "source.toby3d.me/toby3d/auth/internal/health/delivery/http" @@ -295,12 +296,12 @@ func (app *App) Handler() http.Handler { grant.AuthorizationCode, grant.Ticket, }, - CodeChallengeMethodsSupported: []domain.CodeChallengeMethod{ - domain.CodeChallengeMethodMD5, - domain.CodeChallengeMethodPLAIN, - domain.CodeChallengeMethodS1, - domain.CodeChallengeMethodS256, - domain.CodeChallengeMethodS512, + CodeChallengeMethodsSupported: []challenge.Method{ + challenge.MD5, + challenge.PLAIN, + challenge.S1, + challenge.S256, + challenge.S512, }, AuthorizationResponseIssParameterSupported: true, }) diff --git a/web/template/authorize.qtpl b/web/template/authorize.qtpl index acbcf16..1ffeced 100644 --- a/web/template/authorize.qtpl +++ b/web/template/authorize.qtpl @@ -2,6 +2,7 @@ {% import ( "source.toby3d.me/toby3d/auth/internal/domain" + "source.toby3d.me/toby3d/auth/internal/domain/challenge" "source.toby3d.me/toby3d/auth/internal/domain/response" "source.toby3d.me/toby3d/auth/web/template/layout" ) %} @@ -9,7 +10,7 @@ {% code type Authorize struct { layout.BaseOf Scope domain.Scopes - CodeChallengeMethod domain.CodeChallengeMethod + CodeChallengeMethod challenge.Method ResponseType response.Type Client *domain.Client Me *domain.Me @@ -60,7 +61,7 @@