🚚 Moved code challenge method into separated package
This commit is contained in:
parent
cba81b5ac4
commit
71c8373eb4
|
@ -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.
|
||||
|
@ -51,7 +52,7 @@ type (
|
|||
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"`
|
||||
CodeChallengeMethod challenge.Method `form:"code_challenge_method,omitempty"`
|
||||
ResponseType response.Type `form:"response_type"`
|
||||
Authorize string `form:"authorize"`
|
||||
CodeChallenge string `form:"code_challenge,omitempty"`
|
||||
|
@ -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{},
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
@ -37,7 +38,7 @@ type (
|
|||
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"`
|
||||
CodeChallengeMethodsSupported []challenge.Method `json:"code_challenge_methods_supported"`
|
||||
AuthorizationResponseIssParameterSupported bool `json:"authorization_response_iss_parameter_supported,omitempty"`
|
||||
}
|
||||
|
||||
|
|
132
internal/domain/challenge/method.go
Normal file
132
internal/domain/challenge/method.go
Normal file
|
@ -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))
|
||||
}
|
160
internal/domain/challenge/method_test.go
Normal file
160
internal/domain/challenge/method_test.go
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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"},
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"net/url"
|
||||
"testing"
|
||||
|
||||
"source.toby3d.me/toby3d/auth/internal/domain/challenge"
|
||||
"source.toby3d.me/toby3d/auth/internal/random"
|
||||
)
|
||||
|
||||
|
@ -13,7 +14,7 @@ type Session struct {
|
|||
RedirectURI *url.URL `json:"redirect_uri"`
|
||||
Me Me `json:"me"`
|
||||
Profile *Profile `json:"profile,omitempty"`
|
||||
CodeChallengeMethod CodeChallengeMethod `json:"code_challenge_method,omitempty"`
|
||||
CodeChallengeMethod challenge.Method `json:"code_challenge_method,omitempty"`
|
||||
CodeChallenge string `json:"code_challenge,omitempty"`
|
||||
Code string `json:"-"`
|
||||
Scope Scopes `json:"scope"`
|
||||
|
@ -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"},
|
||||
|
|
|
@ -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"
|
||||
|
@ -35,7 +36,7 @@ type (
|
|||
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"`
|
||||
CodeChallengeMethodsSupported []challenge.Method `json:"code_challenge_methods_supported"`
|
||||
AuthorizationResponseIssParameterSupported bool `json:"authorization_response_iss_parameter_supported,omitempty"`
|
||||
}
|
||||
|
||||
|
@ -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),
|
||||
|
|
|
@ -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{},
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
@ -38,7 +39,7 @@ type (
|
|||
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"`
|
||||
CodeChallengeMethodsSupported []challenge.Method `json:"code_challenge_methods_supported"`
|
||||
AuthorizationResponseIssParameterSupported bool `json:"authorization_response_iss_parameter_supported,omitempty"`
|
||||
}
|
||||
|
||||
|
|
13
main.go
13
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,
|
||||
})
|
||||
|
|
|
@ -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 @@
|
|||
|
||||
<main>
|
||||
<aside>
|
||||
{% if p.CodeChallengeMethod != domain.CodeChallengeMethodUnd && p.CodeChallenge != "" %}
|
||||
{% if p.CodeChallengeMethod != challenge.Und && p.CodeChallenge != "" %}
|
||||
<p class="with-icon">
|
||||
<span class="icon"
|
||||
role="img"
|
||||
|
|
|
@ -7,28 +7,29 @@ package template
|
|||
//line web/template/authorize.qtpl:3
|
||||
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"
|
||||
)
|
||||
|
||||
//line web/template/authorize.qtpl:9
|
||||
//line web/template/authorize.qtpl:10
|
||||
import (
|
||||
qtio422016 "io"
|
||||
|
||||
qt422016 "github.com/valyala/quicktemplate"
|
||||
)
|
||||
|
||||
//line web/template/authorize.qtpl:9
|
||||
//line web/template/authorize.qtpl:10
|
||||
var (
|
||||
_ = qtio422016.Copy
|
||||
_ = qt422016.AcquireByteBuffer
|
||||
)
|
||||
|
||||
//line web/template/authorize.qtpl:9
|
||||
//line web/template/authorize.qtpl:10
|
||||
type Authorize struct {
|
||||
layout.BaseOf
|
||||
Scope domain.Scopes
|
||||
CodeChallengeMethod domain.CodeChallengeMethod
|
||||
CodeChallengeMethod challenge.Method
|
||||
ResponseType response.Type
|
||||
Client *domain.Client
|
||||
Me *domain.Me
|
||||
|
@ -39,74 +40,74 @@ type Authorize struct {
|
|||
State string
|
||||
}
|
||||
|
||||
//line web/template/authorize.qtpl:23
|
||||
//line web/template/authorize.qtpl:24
|
||||
func (p *Authorize) StreamTitle(qw422016 *qt422016.Writer) {
|
||||
//line web/template/authorize.qtpl:23
|
||||
//line web/template/authorize.qtpl:24
|
||||
qw422016.N().S(`
|
||||
`)
|
||||
//line web/template/authorize.qtpl:24
|
||||
//line web/template/authorize.qtpl:25
|
||||
if p.Client.Name != "" {
|
||||
//line web/template/authorize.qtpl:24
|
||||
//line web/template/authorize.qtpl:25
|
||||
qw422016.N().S(`
|
||||
`)
|
||||
//line web/template/authorize.qtpl:25
|
||||
//line web/template/authorize.qtpl:26
|
||||
p.StreamT(qw422016, "Authorize %s", p.Client.Name)
|
||||
//line web/template/authorize.qtpl:25
|
||||
//line web/template/authorize.qtpl:26
|
||||
qw422016.N().S(`
|
||||
`)
|
||||
//line web/template/authorize.qtpl:26
|
||||
//line web/template/authorize.qtpl:27
|
||||
} else {
|
||||
//line web/template/authorize.qtpl:26
|
||||
//line web/template/authorize.qtpl:27
|
||||
qw422016.N().S(`
|
||||
`)
|
||||
//line web/template/authorize.qtpl:27
|
||||
//line web/template/authorize.qtpl:28
|
||||
p.StreamT(qw422016, "Authorize application")
|
||||
//line web/template/authorize.qtpl:27
|
||||
qw422016.N().S(`
|
||||
`)
|
||||
//line web/template/authorize.qtpl:28
|
||||
}
|
||||
//line web/template/authorize.qtpl:28
|
||||
qw422016.N().S(`
|
||||
`)
|
||||
//line web/template/authorize.qtpl:29
|
||||
}
|
||||
//line web/template/authorize.qtpl:29
|
||||
qw422016.N().S(`
|
||||
`)
|
||||
//line web/template/authorize.qtpl:30
|
||||
}
|
||||
|
||||
//line web/template/authorize.qtpl:29
|
||||
//line web/template/authorize.qtpl:30
|
||||
func (p *Authorize) WriteTitle(qq422016 qtio422016.Writer) {
|
||||
//line web/template/authorize.qtpl:29
|
||||
//line web/template/authorize.qtpl:30
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line web/template/authorize.qtpl:29
|
||||
//line web/template/authorize.qtpl:30
|
||||
p.StreamTitle(qw422016)
|
||||
//line web/template/authorize.qtpl:29
|
||||
//line web/template/authorize.qtpl:30
|
||||
qt422016.ReleaseWriter(qw422016)
|
||||
//line web/template/authorize.qtpl:29
|
||||
//line web/template/authorize.qtpl:30
|
||||
}
|
||||
|
||||
//line web/template/authorize.qtpl:29
|
||||
//line web/template/authorize.qtpl:30
|
||||
func (p *Authorize) Title() string {
|
||||
//line web/template/authorize.qtpl:29
|
||||
//line web/template/authorize.qtpl:30
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line web/template/authorize.qtpl:29
|
||||
//line web/template/authorize.qtpl:30
|
||||
p.WriteTitle(qb422016)
|
||||
//line web/template/authorize.qtpl:29
|
||||
//line web/template/authorize.qtpl:30
|
||||
qs422016 := string(qb422016.B)
|
||||
//line web/template/authorize.qtpl:29
|
||||
//line web/template/authorize.qtpl:30
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line web/template/authorize.qtpl:29
|
||||
//line web/template/authorize.qtpl:30
|
||||
return qs422016
|
||||
//line web/template/authorize.qtpl:29
|
||||
//line web/template/authorize.qtpl:30
|
||||
}
|
||||
|
||||
//line web/template/authorize.qtpl:31
|
||||
//line web/template/authorize.qtpl:32
|
||||
func (p *Authorize) StreamBody(qw422016 *qt422016.Writer) {
|
||||
//line web/template/authorize.qtpl:31
|
||||
//line web/template/authorize.qtpl:32
|
||||
qw422016.N().S(`
|
||||
<header>
|
||||
`)
|
||||
//line web/template/authorize.qtpl:33
|
||||
//line web/template/authorize.qtpl:34
|
||||
if p.Client.Logo != nil {
|
||||
//line web/template/authorize.qtpl:33
|
||||
//line web/template/authorize.qtpl:34
|
||||
qw422016.N().S(`
|
||||
<img class=""
|
||||
crossorigin="anonymous"
|
||||
|
@ -116,73 +117,73 @@ func (p *Authorize) StreamBody(qw422016 *qt422016.Writer) {
|
|||
loading="lazy"
|
||||
referrerpolicy="no-referrer-when-downgrade"
|
||||
src="`)
|
||||
//line web/template/authorize.qtpl:41
|
||||
//line web/template/authorize.qtpl:42
|
||||
qw422016.E().S(p.Client.Logo.String())
|
||||
//line web/template/authorize.qtpl:41
|
||||
//line web/template/authorize.qtpl:42
|
||||
qw422016.N().S(`"
|
||||
alt="`)
|
||||
//line web/template/authorize.qtpl:42
|
||||
//line web/template/authorize.qtpl:43
|
||||
qw422016.E().S(p.Client.Name)
|
||||
//line web/template/authorize.qtpl:42
|
||||
//line web/template/authorize.qtpl:43
|
||||
qw422016.N().S(`"
|
||||
width="140">
|
||||
`)
|
||||
//line web/template/authorize.qtpl:44
|
||||
//line web/template/authorize.qtpl:45
|
||||
}
|
||||
//line web/template/authorize.qtpl:44
|
||||
//line web/template/authorize.qtpl:45
|
||||
qw422016.N().S(`
|
||||
|
||||
<h2>
|
||||
`)
|
||||
//line web/template/authorize.qtpl:47
|
||||
//line web/template/authorize.qtpl:48
|
||||
if p.Client.URL != nil {
|
||||
//line web/template/authorize.qtpl:47
|
||||
//line web/template/authorize.qtpl:48
|
||||
qw422016.N().S(`
|
||||
<a href="`)
|
||||
//line web/template/authorize.qtpl:48
|
||||
//line web/template/authorize.qtpl:49
|
||||
qw422016.E().S(p.Client.URL.String())
|
||||
//line web/template/authorize.qtpl:48
|
||||
//line web/template/authorize.qtpl:49
|
||||
qw422016.N().S(`">
|
||||
`)
|
||||
//line web/template/authorize.qtpl:49
|
||||
//line web/template/authorize.qtpl:50
|
||||
}
|
||||
//line web/template/authorize.qtpl:49
|
||||
//line web/template/authorize.qtpl:50
|
||||
qw422016.N().S(`
|
||||
`)
|
||||
//line web/template/authorize.qtpl:50
|
||||
//line web/template/authorize.qtpl:51
|
||||
if p.Client.Name != "" {
|
||||
//line web/template/authorize.qtpl:50
|
||||
//line web/template/authorize.qtpl:51
|
||||
qw422016.N().S(`
|
||||
`)
|
||||
//line web/template/authorize.qtpl:51
|
||||
//line web/template/authorize.qtpl:52
|
||||
qw422016.E().S(p.Client.Name)
|
||||
//line web/template/authorize.qtpl:51
|
||||
//line web/template/authorize.qtpl:52
|
||||
qw422016.N().S(`
|
||||
`)
|
||||
//line web/template/authorize.qtpl:52
|
||||
//line web/template/authorize.qtpl:53
|
||||
} else {
|
||||
//line web/template/authorize.qtpl:52
|
||||
//line web/template/authorize.qtpl:53
|
||||
qw422016.N().S(`
|
||||
`)
|
||||
//line web/template/authorize.qtpl:53
|
||||
//line web/template/authorize.qtpl:54
|
||||
qw422016.E().S(p.Client.ID.String())
|
||||
//line web/template/authorize.qtpl:53
|
||||
//line web/template/authorize.qtpl:54
|
||||
qw422016.N().S(`
|
||||
`)
|
||||
//line web/template/authorize.qtpl:54
|
||||
//line web/template/authorize.qtpl:55
|
||||
}
|
||||
//line web/template/authorize.qtpl:54
|
||||
//line web/template/authorize.qtpl:55
|
||||
qw422016.N().S(`
|
||||
`)
|
||||
//line web/template/authorize.qtpl:55
|
||||
//line web/template/authorize.qtpl:56
|
||||
if p.Client.URL != nil {
|
||||
//line web/template/authorize.qtpl:55
|
||||
//line web/template/authorize.qtpl:56
|
||||
qw422016.N().S(`
|
||||
</a>
|
||||
`)
|
||||
//line web/template/authorize.qtpl:57
|
||||
//line web/template/authorize.qtpl:58
|
||||
}
|
||||
//line web/template/authorize.qtpl:57
|
||||
//line web/template/authorize.qtpl:58
|
||||
qw422016.N().S(`
|
||||
</h2>
|
||||
</header>
|
||||
|
@ -190,9 +191,9 @@ func (p *Authorize) StreamBody(qw422016 *qt422016.Writer) {
|
|||
<main>
|
||||
<aside>
|
||||
`)
|
||||
//line web/template/authorize.qtpl:63
|
||||
if p.CodeChallengeMethod != domain.CodeChallengeMethodUnd && p.CodeChallenge != "" {
|
||||
//line web/template/authorize.qtpl:63
|
||||
//line web/template/authorize.qtpl:64
|
||||
if p.CodeChallengeMethod != challenge.Und && p.CodeChallenge != "" {
|
||||
//line web/template/authorize.qtpl:64
|
||||
qw422016.N().S(`
|
||||
<p class="with-icon">
|
||||
<span class="icon"
|
||||
|
@ -200,16 +201,16 @@ func (p *Authorize) StreamBody(qw422016 *qt422016.Writer) {
|
|||
aria-label="closed lock with key">🔐</span>
|
||||
|
||||
`)
|
||||
//line web/template/authorize.qtpl:69
|
||||
//line web/template/authorize.qtpl:70
|
||||
p.StreamT(qw422016, `This client uses %sPKCE%s with the %s%s%s method.`, `<abbr title="Proof of Key Code Exchange">`,
|
||||
`</abbr>`, `<code>`, p.CodeChallengeMethod, `</code>`)
|
||||
//line web/template/authorize.qtpl:70
|
||||
//line web/template/authorize.qtpl:71
|
||||
qw422016.N().S(`
|
||||
</p>
|
||||
`)
|
||||
//line web/template/authorize.qtpl:72
|
||||
//line web/template/authorize.qtpl:73
|
||||
} else {
|
||||
//line web/template/authorize.qtpl:72
|
||||
//line web/template/authorize.qtpl:73
|
||||
qw422016.N().S(`
|
||||
<details>
|
||||
<summary class="with-icon">
|
||||
|
@ -218,26 +219,26 @@ func (p *Authorize) StreamBody(qw422016 *qt422016.Writer) {
|
|||
aria-label="unlock">🔓</span>
|
||||
|
||||
`)
|
||||
//line web/template/authorize.qtpl:79
|
||||
//line web/template/authorize.qtpl:80
|
||||
p.StreamT(qw422016, `This client does not use %sPKCE%s!`, `<abbr title="Proof of Key Code Exchange">`, `</abbr>`)
|
||||
//line web/template/authorize.qtpl:79
|
||||
//line web/template/authorize.qtpl:80
|
||||
qw422016.N().S(`
|
||||
</summary>
|
||||
<p>
|
||||
`)
|
||||
//line web/template/authorize.qtpl:82
|
||||
//line web/template/authorize.qtpl:83
|
||||
p.StreamT(qw422016, `%sProof of Key Code Exchange%s is a mechanism that protects against attackers in the middle hijacking `+
|
||||
`your application's authentication process. You can still authorize this application without this protection, `+
|
||||
`but you must independently verify the security of this connection. If you have any doubts - stop the process `+
|
||||
` and contact the developers.`, `<dfn id="PKCE">`, `</dfn>`)
|
||||
//line web/template/authorize.qtpl:85
|
||||
//line web/template/authorize.qtpl:86
|
||||
qw422016.N().S(`
|
||||
</p>
|
||||
</details>
|
||||
`)
|
||||
//line web/template/authorize.qtpl:88
|
||||
//line web/template/authorize.qtpl:89
|
||||
}
|
||||
//line web/template/authorize.qtpl:88
|
||||
//line web/template/authorize.qtpl:89
|
||||
qw422016.N().S(`
|
||||
</aside>
|
||||
|
||||
|
@ -251,215 +252,215 @@ func (p *Authorize) StreamBody(qw422016 *qt422016.Writer) {
|
|||
target="_self">
|
||||
|
||||
`)
|
||||
//line web/template/authorize.qtpl:100
|
||||
//line web/template/authorize.qtpl:101
|
||||
if p.CSRF != nil {
|
||||
//line web/template/authorize.qtpl:100
|
||||
//line web/template/authorize.qtpl:101
|
||||
qw422016.N().S(`
|
||||
<input type="hidden"
|
||||
name="_csrf"
|
||||
value="`)
|
||||
//line web/template/authorize.qtpl:103
|
||||
//line web/template/authorize.qtpl:104
|
||||
qw422016.E().Z(p.CSRF)
|
||||
//line web/template/authorize.qtpl:103
|
||||
//line web/template/authorize.qtpl:104
|
||||
qw422016.N().S(`">
|
||||
`)
|
||||
//line web/template/authorize.qtpl:104
|
||||
//line web/template/authorize.qtpl:105
|
||||
}
|
||||
//line web/template/authorize.qtpl:104
|
||||
//line web/template/authorize.qtpl:105
|
||||
qw422016.N().S(`
|
||||
|
||||
`)
|
||||
//line web/template/authorize.qtpl:106
|
||||
//line web/template/authorize.qtpl:107
|
||||
for key, val := range map[string]string{
|
||||
"client_id": p.Client.ID.String(),
|
||||
"redirect_uri": p.RedirectURI.String(),
|
||||
"response_type": p.ResponseType.String(),
|
||||
"state": p.State,
|
||||
} {
|
||||
//line web/template/authorize.qtpl:111
|
||||
//line web/template/authorize.qtpl:112
|
||||
qw422016.N().S(`
|
||||
<input type="hidden"
|
||||
name="`)
|
||||
//line web/template/authorize.qtpl:113
|
||||
//line web/template/authorize.qtpl:114
|
||||
qw422016.E().S(key)
|
||||
//line web/template/authorize.qtpl:113
|
||||
//line web/template/authorize.qtpl:114
|
||||
qw422016.N().S(`"
|
||||
value="`)
|
||||
//line web/template/authorize.qtpl:114
|
||||
//line web/template/authorize.qtpl:115
|
||||
qw422016.E().S(val)
|
||||
//line web/template/authorize.qtpl:114
|
||||
//line web/template/authorize.qtpl:115
|
||||
qw422016.N().S(`">
|
||||
`)
|
||||
//line web/template/authorize.qtpl:115
|
||||
//line web/template/authorize.qtpl:116
|
||||
}
|
||||
//line web/template/authorize.qtpl:115
|
||||
//line web/template/authorize.qtpl:116
|
||||
qw422016.N().S(`
|
||||
|
||||
`)
|
||||
//line web/template/authorize.qtpl:117
|
||||
//line web/template/authorize.qtpl:118
|
||||
if len(p.Scope) > 0 {
|
||||
//line web/template/authorize.qtpl:117
|
||||
//line web/template/authorize.qtpl:118
|
||||
qw422016.N().S(`
|
||||
<fieldset>
|
||||
<legend>`)
|
||||
//line web/template/authorize.qtpl:119
|
||||
//line web/template/authorize.qtpl:120
|
||||
p.StreamT(qw422016, "Scopes")
|
||||
//line web/template/authorize.qtpl:119
|
||||
//line web/template/authorize.qtpl:120
|
||||
qw422016.N().S(`</legend>
|
||||
|
||||
`)
|
||||
//line web/template/authorize.qtpl:121
|
||||
//line web/template/authorize.qtpl:122
|
||||
for _, scope := range p.Scope {
|
||||
//line web/template/authorize.qtpl:121
|
||||
//line web/template/authorize.qtpl:122
|
||||
qw422016.N().S(`
|
||||
<div>
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
name="scope[]"
|
||||
value="`)
|
||||
//line web/template/authorize.qtpl:126
|
||||
//line web/template/authorize.qtpl:127
|
||||
qw422016.E().S(scope.String())
|
||||
//line web/template/authorize.qtpl:126
|
||||
//line web/template/authorize.qtpl:127
|
||||
qw422016.N().S(`"
|
||||
checked>
|
||||
|
||||
`)
|
||||
//line web/template/authorize.qtpl:129
|
||||
//line web/template/authorize.qtpl:130
|
||||
qw422016.E().S(scope.String())
|
||||
//line web/template/authorize.qtpl:129
|
||||
//line web/template/authorize.qtpl:130
|
||||
qw422016.N().S(`
|
||||
</label>
|
||||
</div>
|
||||
`)
|
||||
//line web/template/authorize.qtpl:132
|
||||
//line web/template/authorize.qtpl:133
|
||||
}
|
||||
//line web/template/authorize.qtpl:132
|
||||
//line web/template/authorize.qtpl:133
|
||||
qw422016.N().S(`
|
||||
</fieldset>
|
||||
`)
|
||||
//line web/template/authorize.qtpl:134
|
||||
//line web/template/authorize.qtpl:135
|
||||
} else {
|
||||
//line web/template/authorize.qtpl:134
|
||||
//line web/template/authorize.qtpl:135
|
||||
qw422016.N().S(`
|
||||
<aside>
|
||||
<p>`)
|
||||
//line web/template/authorize.qtpl:136
|
||||
//line web/template/authorize.qtpl:137
|
||||
p.StreamT(qw422016, `No scopes is requested: the application will only get your profile URL.`)
|
||||
//line web/template/authorize.qtpl:136
|
||||
//line web/template/authorize.qtpl:137
|
||||
qw422016.N().S(`</p>
|
||||
</aside>
|
||||
`)
|
||||
//line web/template/authorize.qtpl:138
|
||||
//line web/template/authorize.qtpl:139
|
||||
}
|
||||
//line web/template/authorize.qtpl:138
|
||||
//line web/template/authorize.qtpl:139
|
||||
qw422016.N().S(`
|
||||
|
||||
`)
|
||||
//line web/template/authorize.qtpl:140
|
||||
//line web/template/authorize.qtpl:141
|
||||
if p.CodeChallenge != "" {
|
||||
//line web/template/authorize.qtpl:140
|
||||
//line web/template/authorize.qtpl:141
|
||||
qw422016.N().S(`
|
||||
`)
|
||||
//line web/template/authorize.qtpl:141
|
||||
//line web/template/authorize.qtpl:142
|
||||
for key, val := range map[string]string{
|
||||
"code_challenge": p.CodeChallenge,
|
||||
"code_challenge_method": p.CodeChallengeMethod.String(),
|
||||
} {
|
||||
//line web/template/authorize.qtpl:144
|
||||
//line web/template/authorize.qtpl:145
|
||||
qw422016.N().S(`
|
||||
<input type="hidden"
|
||||
name="`)
|
||||
//line web/template/authorize.qtpl:146
|
||||
//line web/template/authorize.qtpl:147
|
||||
qw422016.E().S(key)
|
||||
//line web/template/authorize.qtpl:146
|
||||
//line web/template/authorize.qtpl:147
|
||||
qw422016.N().S(`"
|
||||
value="`)
|
||||
//line web/template/authorize.qtpl:147
|
||||
//line web/template/authorize.qtpl:148
|
||||
qw422016.E().S(val)
|
||||
//line web/template/authorize.qtpl:147
|
||||
//line web/template/authorize.qtpl:148
|
||||
qw422016.N().S(`">
|
||||
`)
|
||||
//line web/template/authorize.qtpl:148
|
||||
//line web/template/authorize.qtpl:149
|
||||
}
|
||||
//line web/template/authorize.qtpl:148
|
||||
//line web/template/authorize.qtpl:149
|
||||
qw422016.N().S(`
|
||||
`)
|
||||
//line web/template/authorize.qtpl:149
|
||||
//line web/template/authorize.qtpl:150
|
||||
}
|
||||
//line web/template/authorize.qtpl:149
|
||||
//line web/template/authorize.qtpl:150
|
||||
qw422016.N().S(`
|
||||
|
||||
`)
|
||||
//line web/template/authorize.qtpl:151
|
||||
//line web/template/authorize.qtpl:152
|
||||
if p.Me != nil {
|
||||
//line web/template/authorize.qtpl:151
|
||||
//line web/template/authorize.qtpl:152
|
||||
qw422016.N().S(`
|
||||
<input type="hidden"
|
||||
name="me"
|
||||
value="`)
|
||||
//line web/template/authorize.qtpl:154
|
||||
//line web/template/authorize.qtpl:155
|
||||
qw422016.E().S(p.Me.String())
|
||||
//line web/template/authorize.qtpl:154
|
||||
//line web/template/authorize.qtpl:155
|
||||
qw422016.N().S(`">
|
||||
`)
|
||||
//line web/template/authorize.qtpl:155
|
||||
//line web/template/authorize.qtpl:156
|
||||
}
|
||||
//line web/template/authorize.qtpl:155
|
||||
//line web/template/authorize.qtpl:156
|
||||
qw422016.N().S(`
|
||||
|
||||
`)
|
||||
//line web/template/authorize.qtpl:157
|
||||
//line web/template/authorize.qtpl:158
|
||||
if len(p.Providers) > 0 {
|
||||
//line web/template/authorize.qtpl:157
|
||||
//line web/template/authorize.qtpl:158
|
||||
qw422016.N().S(`
|
||||
<select name="provider"
|
||||
autocomplete
|
||||
required>
|
||||
|
||||
`)
|
||||
//line web/template/authorize.qtpl:162
|
||||
//line web/template/authorize.qtpl:163
|
||||
for _, provider := range p.Providers {
|
||||
//line web/template/authorize.qtpl:162
|
||||
//line web/template/authorize.qtpl:163
|
||||
qw422016.N().S(`
|
||||
<option value="`)
|
||||
//line web/template/authorize.qtpl:163
|
||||
//line web/template/authorize.qtpl:164
|
||||
qw422016.E().S(provider.UID)
|
||||
//line web/template/authorize.qtpl:163
|
||||
//line web/template/authorize.qtpl:164
|
||||
qw422016.N().S(`"
|
||||
`)
|
||||
//line web/template/authorize.qtpl:164
|
||||
//line web/template/authorize.qtpl:165
|
||||
if provider.UID == "mastodon" {
|
||||
//line web/template/authorize.qtpl:164
|
||||
//line web/template/authorize.qtpl:165
|
||||
qw422016.N().S(`selected`)
|
||||
//line web/template/authorize.qtpl:164
|
||||
//line web/template/authorize.qtpl:165
|
||||
}
|
||||
//line web/template/authorize.qtpl:164
|
||||
//line web/template/authorize.qtpl:165
|
||||
qw422016.N().S(`>
|
||||
|
||||
`)
|
||||
//line web/template/authorize.qtpl:166
|
||||
//line web/template/authorize.qtpl:167
|
||||
qw422016.E().S(provider.Name)
|
||||
//line web/template/authorize.qtpl:166
|
||||
//line web/template/authorize.qtpl:167
|
||||
qw422016.N().S(`
|
||||
</option>
|
||||
`)
|
||||
//line web/template/authorize.qtpl:168
|
||||
//line web/template/authorize.qtpl:169
|
||||
}
|
||||
//line web/template/authorize.qtpl:168
|
||||
//line web/template/authorize.qtpl:169
|
||||
qw422016.N().S(`
|
||||
</select>
|
||||
`)
|
||||
//line web/template/authorize.qtpl:170
|
||||
//line web/template/authorize.qtpl:171
|
||||
} else {
|
||||
//line web/template/authorize.qtpl:170
|
||||
//line web/template/authorize.qtpl:171
|
||||
qw422016.N().S(`
|
||||
<input type="hidden"
|
||||
name="provider"
|
||||
value="direct">
|
||||
`)
|
||||
//line web/template/authorize.qtpl:174
|
||||
//line web/template/authorize.qtpl:175
|
||||
}
|
||||
//line web/template/authorize.qtpl:174
|
||||
//line web/template/authorize.qtpl:175
|
||||
qw422016.N().S(`
|
||||
|
||||
<button type="submit"
|
||||
|
@ -467,9 +468,9 @@ func (p *Authorize) StreamBody(qw422016 *qt422016.Writer) {
|
|||
value="deny">
|
||||
|
||||
`)
|
||||
//line web/template/authorize.qtpl:180
|
||||
//line web/template/authorize.qtpl:181
|
||||
p.StreamT(qw422016, "Deny")
|
||||
//line web/template/authorize.qtpl:180
|
||||
//line web/template/authorize.qtpl:181
|
||||
qw422016.N().S(`
|
||||
</button>
|
||||
|
||||
|
@ -478,47 +479,47 @@ func (p *Authorize) StreamBody(qw422016 *qt422016.Writer) {
|
|||
value="allow">
|
||||
|
||||
`)
|
||||
//line web/template/authorize.qtpl:187
|
||||
//line web/template/authorize.qtpl:188
|
||||
p.StreamT(qw422016, "Allow")
|
||||
//line web/template/authorize.qtpl:187
|
||||
//line web/template/authorize.qtpl:188
|
||||
qw422016.N().S(`
|
||||
</button>
|
||||
|
||||
<aside>
|
||||
<p>`)
|
||||
//line web/template/authorize.qtpl:191
|
||||
//line web/template/authorize.qtpl:192
|
||||
p.StreamT(qw422016, `You will be redirected to %s%s%s`, `<code>`, p.RedirectURI, `</code>`)
|
||||
//line web/template/authorize.qtpl:191
|
||||
//line web/template/authorize.qtpl:192
|
||||
qw422016.N().S(`</p>
|
||||
</aside>
|
||||
</form>
|
||||
</main>
|
||||
`)
|
||||
//line web/template/authorize.qtpl:195
|
||||
//line web/template/authorize.qtpl:196
|
||||
}
|
||||
|
||||
//line web/template/authorize.qtpl:195
|
||||
//line web/template/authorize.qtpl:196
|
||||
func (p *Authorize) WriteBody(qq422016 qtio422016.Writer) {
|
||||
//line web/template/authorize.qtpl:195
|
||||
//line web/template/authorize.qtpl:196
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line web/template/authorize.qtpl:195
|
||||
//line web/template/authorize.qtpl:196
|
||||
p.StreamBody(qw422016)
|
||||
//line web/template/authorize.qtpl:195
|
||||
//line web/template/authorize.qtpl:196
|
||||
qt422016.ReleaseWriter(qw422016)
|
||||
//line web/template/authorize.qtpl:195
|
||||
//line web/template/authorize.qtpl:196
|
||||
}
|
||||
|
||||
//line web/template/authorize.qtpl:195
|
||||
//line web/template/authorize.qtpl:196
|
||||
func (p *Authorize) Body() string {
|
||||
//line web/template/authorize.qtpl:195
|
||||
//line web/template/authorize.qtpl:196
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line web/template/authorize.qtpl:195
|
||||
//line web/template/authorize.qtpl:196
|
||||
p.WriteBody(qb422016)
|
||||
//line web/template/authorize.qtpl:195
|
||||
//line web/template/authorize.qtpl:196
|
||||
qs422016 := string(qb422016.B)
|
||||
//line web/template/authorize.qtpl:195
|
||||
//line web/template/authorize.qtpl:196
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line web/template/authorize.qtpl:195
|
||||
//line web/template/authorize.qtpl:196
|
||||
return qs422016
|
||||
//line web/template/authorize.qtpl:195
|
||||
//line web/template/authorize.qtpl:196
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user