auth/internal/domain/code_challenge_method.go

131 lines
3.8 KiB
Go

package domain
//nolint:gosec // support old clients
import (
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"encoding/base64"
"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_challene_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[strings.ToLower(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 {
method, err := ParseCodeChallengeMethod(string(v))
if err != nil {
return fmt.Errorf("CodeChallengeMethod: UnmarshalForm: %w", err)
}
*ccm = method
return nil
}
// UnmarshalJSON implements custom unmarshler for JSON.
func (ccm *CodeChallengeMethod) UnmarshalJSON(v []byte) error {
src, err := strconv.Unquote(string(v))
if err != nil {
return fmt.Errorf("CodeChallengeMethod: UnmarshalJSON: %w", err)
}
if *ccm, err = ParseCodeChallengeMethod(src); err != nil {
return fmt.Errorf("CodeChallengeMethod: UnmarshalJSON: %w", err)
}
return nil
}
func (ccm CodeChallengeMethod) MarshalJSON() ([]byte, error) {
return []byte(strconv.Quote(ccm.codeChallengeMethod)), nil
}
// String returns string representation of code challenge method.
func (ccm CodeChallengeMethod) String() string {
if ccm.codeChallengeMethod != "" {
return strings.ToUpper(ccm.codeChallengeMethod)
}
return common.Und
}
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))
}