🎨 Format domain package code
This commit is contained in:
parent
f6174c67e0
commit
14f4d7d2ef
|
@ -1,44 +1,49 @@
|
|||
package domain
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Action represent action for token endpoint supported by IndieAuth.
|
||||
//
|
||||
// NOTE(toby3d): Encapsulate enums in structs for extra compile-time safety:
|
||||
// https://threedots.tech/post/safer-enums-in-go/#struct-based-enums
|
||||
type Action struct {
|
||||
slug string
|
||||
uid string
|
||||
}
|
||||
|
||||
//nolint: gochecknoglobals // NOTE(toby3d): structs cannot be constants
|
||||
var (
|
||||
ActionUndefined = Action{slug: ""}
|
||||
ActionRevoke = Action{slug: "revoke"}
|
||||
ActionTicket = Action{slug: "ticket"}
|
||||
ActionUndefined = Action{uid: ""}
|
||||
|
||||
// ActionRevoke represent action for revoke token.
|
||||
ActionRevoke = Action{uid: "revoke"}
|
||||
|
||||
// ActionTicket represent action for TicketAuth extension.
|
||||
ActionTicket = Action{uid: "ticket"}
|
||||
)
|
||||
|
||||
var ErrActionUnknown = errors.New("unknown action method")
|
||||
var ErrActionUnknown error = NewError(ErrorCodeInvalidRequest, "unknown action method")
|
||||
|
||||
// ParseAction parse string identifier of action into struct enum.
|
||||
func ParseAction(slug string) (Action, error) {
|
||||
switch strings.ToLower(slug) {
|
||||
case ActionRevoke.slug:
|
||||
func ParseAction(uid string) (Action, error) {
|
||||
switch strings.ToLower(uid) {
|
||||
case ActionRevoke.uid:
|
||||
return ActionRevoke, nil
|
||||
case ActionTicket.slug:
|
||||
case ActionTicket.uid:
|
||||
return ActionTicket, nil
|
||||
}
|
||||
|
||||
return ActionUndefined, fmt.Errorf("%w: %s", ErrActionUnknown, slug)
|
||||
return ActionUndefined, fmt.Errorf("%w: %s", ErrActionUnknown, uid)
|
||||
}
|
||||
|
||||
// UnmarshalForm implements custom unmarshler for form values.
|
||||
func (a *Action) UnmarshalForm(v []byte) error {
|
||||
action, err := ParseAction(string(v))
|
||||
if err != nil {
|
||||
return fmt.Errorf("action: %w", err)
|
||||
return fmt.Errorf("UnmarshalForm: %w", err)
|
||||
}
|
||||
|
||||
*a = action
|
||||
|
@ -46,15 +51,16 @@ func (a *Action) UnmarshalForm(v []byte) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements custom unmarshler for JSON.
|
||||
func (a *Action) UnmarshalJSON(v []byte) error {
|
||||
src, err := strconv.Unquote(string(v))
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("UnmarshalJSON: %w", err)
|
||||
}
|
||||
|
||||
action, err := ParseAction(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("action: %w", err)
|
||||
return fmt.Errorf("UnmarshalJSON: %w", err)
|
||||
}
|
||||
|
||||
*a = action
|
||||
|
@ -64,5 +70,5 @@ func (a *Action) UnmarshalJSON(v []byte) error {
|
|||
|
||||
// String returns string representation of action.
|
||||
func (a Action) String() string {
|
||||
return a.slug
|
||||
return a.uid
|
||||
}
|
||||
|
|
|
@ -16,7 +16,18 @@ type Client struct {
|
|||
Name []string
|
||||
}
|
||||
|
||||
// TestClient returns a valid Client with the generated test data filled in.
|
||||
// NewClient creates a new empty Client with provided ClientID, if any.
|
||||
func NewClient(cid *ClientID) *Client {
|
||||
return &Client{
|
||||
ID: cid,
|
||||
Logo: make([]*URL, 0),
|
||||
RedirectURI: make([]*URL, 0),
|
||||
URL: make([]*URL, 0),
|
||||
Name: make([]string, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// TestClient returns valid random generated client for tests.
|
||||
func TestClient(tb testing.TB) *Client {
|
||||
tb.Helper()
|
||||
|
||||
|
@ -76,6 +87,7 @@ func (c *Client) ValidateRedirectURI(redirectURI *URL) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// GetName safe returns first name, if any.
|
||||
func (c Client) GetName() string {
|
||||
if len(c.Name) < 1 {
|
||||
return ""
|
||||
|
@ -84,6 +96,7 @@ func (c Client) GetName() string {
|
|||
return c.Name[0]
|
||||
}
|
||||
|
||||
// GetURL safe returns first uRL, if any.
|
||||
func (c Client) GetURL() *URL {
|
||||
if len(c.URL) < 1 {
|
||||
return nil
|
||||
|
@ -92,6 +105,7 @@ func (c Client) GetURL() *URL {
|
|||
return c.URL[0]
|
||||
}
|
||||
|
||||
// GetLogo safe returns first logo, if any.
|
||||
func (c Client) GetLogo() *URL {
|
||||
if len(c.Logo) < 1 {
|
||||
return nil
|
||||
|
|
|
@ -25,63 +25,70 @@ var (
|
|||
localhostIPv6 = netaddr.MustParseIP("::1")
|
||||
)
|
||||
|
||||
// ParseClientID parse string as client ID URL identifier.
|
||||
//nolint: funlen
|
||||
func ParseClientID(raw string) (*ClientID, error) {
|
||||
clientID := http.AcquireURI()
|
||||
if err := clientID.Parse(nil, []byte(raw)); err != nil {
|
||||
func ParseClientID(src string) (*ClientID, error) {
|
||||
cid := http.AcquireURI()
|
||||
if err := cid.Parse(nil, []byte(src)); err != nil {
|
||||
return nil, Error{
|
||||
Code: ErrorCodeInvalidRequest,
|
||||
Description: err.Error(),
|
||||
URI: "https://indieauth.net/source/#client-identifier",
|
||||
State: "",
|
||||
frame: xerrors.Caller(1),
|
||||
}
|
||||
}
|
||||
|
||||
scheme := string(clientID.Scheme())
|
||||
scheme := string(cid.Scheme())
|
||||
if scheme != "http" && scheme != "https" {
|
||||
return nil, Error{
|
||||
Code: ErrorCodeInvalidRequest,
|
||||
Description: "client identifier URL MUST have either an https or http scheme",
|
||||
URI: "https://indieauth.net/source/#client-identifier",
|
||||
State: "",
|
||||
frame: xerrors.Caller(1),
|
||||
}
|
||||
}
|
||||
|
||||
path := string(clientID.PathOriginal())
|
||||
path := string(cid.PathOriginal())
|
||||
if path == "" || strings.Contains(path, "/.") || strings.Contains(path, "/..") {
|
||||
return nil, Error{
|
||||
Code: ErrorCodeInvalidRequest,
|
||||
Description: "client identifier URL MUST contain a path component and MUST NOT contain " +
|
||||
"single-dot or double-dot path segments",
|
||||
URI: "https://indieauth.net/source/#client-identifier",
|
||||
State: "",
|
||||
frame: xerrors.Caller(1),
|
||||
}
|
||||
}
|
||||
|
||||
if clientID.Hash() != nil {
|
||||
if cid.Hash() != nil {
|
||||
return nil, Error{
|
||||
Code: ErrorCodeInvalidRequest,
|
||||
Description: "client identifier URL MUST NOT contain a fragment component",
|
||||
URI: "https://indieauth.net/source/#client-identifier",
|
||||
State: "",
|
||||
frame: xerrors.Caller(1),
|
||||
}
|
||||
}
|
||||
|
||||
if clientID.Username() != nil || clientID.Password() != nil {
|
||||
if cid.Username() != nil || cid.Password() != nil {
|
||||
return nil, Error{
|
||||
Code: ErrorCodeInvalidRequest,
|
||||
Description: "client identifier URL MUST NOT contain a username or password component",
|
||||
URI: "https://indieauth.net/source/#client-identifier",
|
||||
State: "",
|
||||
frame: xerrors.Caller(1),
|
||||
}
|
||||
}
|
||||
|
||||
domain := string(clientID.Host())
|
||||
domain := string(cid.Host())
|
||||
if domain == "" {
|
||||
return nil, Error{
|
||||
Code: ErrorCodeInvalidRequest,
|
||||
Description: "client host name MUST be domain name or a loopback interface",
|
||||
URI: "https://indieauth.net/source/#client-identifier",
|
||||
State: "",
|
||||
frame: xerrors.Caller(1),
|
||||
}
|
||||
}
|
||||
|
@ -90,7 +97,9 @@ func ParseClientID(raw string) (*ClientID, error) {
|
|||
if err != nil {
|
||||
ipPort, err := netaddr.ParseIPPort(domain)
|
||||
if err != nil {
|
||||
return &ClientID{clientID: clientID}, nil
|
||||
return &ClientID{
|
||||
clientID: cid,
|
||||
}, nil
|
||||
}
|
||||
|
||||
ip = ipPort.IP()
|
||||
|
@ -102,14 +111,17 @@ func ParseClientID(raw string) (*ClientID, error) {
|
|||
Description: "client identifier URL MUST NOT be IPv4 or IPv6 addresses except for IPv4 " +
|
||||
"127.0.0.1 or IPv6 [::1]",
|
||||
URI: "https://indieauth.net/source/#client-identifier",
|
||||
State: "",
|
||||
frame: xerrors.Caller(1),
|
||||
}
|
||||
}
|
||||
|
||||
return &ClientID{clientID: clientID}, nil
|
||||
return &ClientID{
|
||||
clientID: cid,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// TestClientID returns a valid random generated ClientID for tests.
|
||||
// TestClientID returns valid random generated ClientID for tests.
|
||||
func TestClientID(tb testing.TB) *ClientID {
|
||||
tb.Helper()
|
||||
|
||||
|
@ -119,7 +131,7 @@ func TestClientID(tb testing.TB) *ClientID {
|
|||
return clientID
|
||||
}
|
||||
|
||||
// UnmarshalForm implements a custom form.Unmarshaler.
|
||||
// UnmarshalForm implements custom unmarshler for form values.
|
||||
func (cid *ClientID) UnmarshalForm(v []byte) error {
|
||||
clientID, err := ParseClientID(string(v))
|
||||
if err != nil {
|
||||
|
@ -131,10 +143,11 @@ func (cid *ClientID) UnmarshalForm(v []byte) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements custom unmarshler for JSON.
|
||||
func (cid *ClientID) UnmarshalJSON(v []byte) error {
|
||||
src, err := strconv.Unquote(string(v))
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("UnmarshalJSON: %w", err)
|
||||
}
|
||||
|
||||
clientID, err := ParseClientID(src)
|
||||
|
@ -147,6 +160,7 @@ func (cid *ClientID) UnmarshalJSON(v []byte) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// MarshalForm implements custom marshler for JSON.
|
||||
func (cid ClientID) MarshalJSON() ([]byte, error) {
|
||||
return []byte(strconv.Quote(cid.String())), nil
|
||||
}
|
||||
|
@ -160,6 +174,7 @@ func (cid ClientID) URI() *http.URI {
|
|||
return u
|
||||
}
|
||||
|
||||
// URL returns url.URL representation of client ID.
|
||||
func (cid ClientID) URL() *url.URL {
|
||||
return &url.URL{
|
||||
Scheme: string(cid.clientID.Scheme()),
|
||||
|
|
|
@ -6,79 +6,80 @@ import (
|
|||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
hash hash.Hash
|
||||
slug string
|
||||
uid string
|
||||
}
|
||||
|
||||
//nolint: gochecknoglobals // NOTE(toby3d): structs cannot be constants
|
||||
var (
|
||||
CodeChallengeMethodUndefined = CodeChallengeMethod{
|
||||
slug: "",
|
||||
uid: "",
|
||||
hash: nil,
|
||||
}
|
||||
|
||||
CodeChallengeMethodPLAIN = CodeChallengeMethod{
|
||||
slug: "PLAIN",
|
||||
uid: "PLAIN",
|
||||
hash: nil,
|
||||
}
|
||||
|
||||
CodeChallengeMethodMD5 = CodeChallengeMethod{
|
||||
slug: "MD5",
|
||||
uid: "MD5",
|
||||
hash: md5.New(),
|
||||
}
|
||||
|
||||
CodeChallengeMethodS1 = CodeChallengeMethod{
|
||||
slug: "S1",
|
||||
uid: "S1",
|
||||
hash: sha1.New(),
|
||||
}
|
||||
|
||||
CodeChallengeMethodS256 = CodeChallengeMethod{
|
||||
slug: "S256",
|
||||
uid: "S256",
|
||||
hash: sha256.New(),
|
||||
}
|
||||
|
||||
CodeChallengeMethodS512 = CodeChallengeMethod{
|
||||
slug: "S512",
|
||||
uid: "S512",
|
||||
hash: sha512.New(),
|
||||
}
|
||||
)
|
||||
|
||||
var ErrCodeChallengeMethodUnknown = errors.New("unknown code challenge method")
|
||||
var ErrCodeChallengeMethodUnknown error = NewError(ErrorCodeInvalidRequest, "unknown code_challene_method")
|
||||
|
||||
//nolint: gochecknoglobals // NOTE(toby3d): maps cannot be constants
|
||||
var slugsMethods = map[string]CodeChallengeMethod{
|
||||
CodeChallengeMethodMD5.slug: CodeChallengeMethodMD5,
|
||||
CodeChallengeMethodPLAIN.slug: CodeChallengeMethodPLAIN,
|
||||
CodeChallengeMethodS1.slug: CodeChallengeMethodS1,
|
||||
CodeChallengeMethodS256.slug: CodeChallengeMethodS256,
|
||||
CodeChallengeMethodS512.slug: CodeChallengeMethodS512,
|
||||
CodeChallengeMethodMD5.uid: CodeChallengeMethodMD5,
|
||||
CodeChallengeMethodPLAIN.uid: CodeChallengeMethodPLAIN,
|
||||
CodeChallengeMethodS1.uid: CodeChallengeMethodS1,
|
||||
CodeChallengeMethodS256.uid: CodeChallengeMethodS256,
|
||||
CodeChallengeMethodS512.uid: CodeChallengeMethodS512,
|
||||
}
|
||||
|
||||
// ParseCodeChallengeMethod parse string identifier of code challenge method
|
||||
// into struct enum.
|
||||
func ParseCodeChallengeMethod(slug string) (CodeChallengeMethod, error) {
|
||||
if method, ok := slugsMethods[strings.ToUpper(slug)]; ok {
|
||||
func ParseCodeChallengeMethod(uid string) (CodeChallengeMethod, error) {
|
||||
if method, ok := slugsMethods[strings.ToUpper(uid)]; ok {
|
||||
return method, nil
|
||||
}
|
||||
|
||||
return CodeChallengeMethodUndefined, fmt.Errorf("%w: %s", ErrCodeChallengeMethodUnknown, slug)
|
||||
return CodeChallengeMethodUndefined, 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("code_challenge_method: %w", err)
|
||||
return fmt.Errorf("UnmarshalForm: %w", err)
|
||||
}
|
||||
|
||||
*ccm = method
|
||||
|
@ -86,15 +87,16 @@ func (ccm *CodeChallengeMethod) UnmarshalForm(v []byte) error {
|
|||
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 err
|
||||
return fmt.Errorf("UnmarshalJSON: %w", err)
|
||||
}
|
||||
|
||||
method, err := ParseCodeChallengeMethod(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("code_challenge_method: %w", err)
|
||||
return fmt.Errorf("UnmarshalJSON: %w", err)
|
||||
}
|
||||
|
||||
*ccm = method
|
||||
|
@ -104,15 +106,17 @@ func (ccm *CodeChallengeMethod) UnmarshalJSON(v []byte) error {
|
|||
|
||||
// String returns string representation of code challenge method.
|
||||
func (ccm CodeChallengeMethod) String() string {
|
||||
return ccm.slug
|
||||
return ccm.uid
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if ccm.slug == CodeChallengeMethodUndefined.slug {
|
||||
if ccm.uid == CodeChallengeMethodUndefined.uid {
|
||||
return false
|
||||
}
|
||||
|
||||
if ccm.slug == CodeChallengeMethodPLAIN.slug {
|
||||
if ccm.uid == CodeChallengeMethodPLAIN.uid {
|
||||
return codeChallenge == verifier
|
||||
}
|
||||
|
||||
|
|
|
@ -62,9 +62,20 @@ type (
|
|||
Expiry time.Duration `yaml:"expiry"` // 1m
|
||||
Length int `yaml:"length"` // 24
|
||||
}
|
||||
|
||||
ConfigRelMeAuth struct {
|
||||
Enabled bool `yaml:"enabled"` // true
|
||||
Providers []ConfigRelMeAuthProvider `yaml:"providers"`
|
||||
}
|
||||
|
||||
ConfigRelMeAuthProvider struct {
|
||||
Type string `yaml:"type"`
|
||||
ID string `yaml:"id"`
|
||||
Secret string `yaml:"secret"`
|
||||
}
|
||||
)
|
||||
|
||||
// TestConfig returns a valid *viper.Viper with the generated test data filled in.
|
||||
// TestConfig returns a valid config for tests.
|
||||
func TestConfig(tb testing.TB) *Config {
|
||||
tb.Helper()
|
||||
|
||||
|
@ -112,7 +123,7 @@ func (cs ConfigServer) GetAddress() string {
|
|||
return net.JoinHostPort(cs.Host, cs.Port)
|
||||
}
|
||||
|
||||
// GetRootURL returns generated from template RootURL.
|
||||
// GetRootURL returns generated root URL from template RootURL.
|
||||
func (cs ConfigServer) GetRootURL() string {
|
||||
return fasttemplate.ExecuteString(cs.RootURL, `{{`, `}}`, map[string]interface{}{
|
||||
"domain": cs.Domain,
|
||||
|
|
|
@ -3,23 +3,17 @@ package domain
|
|||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
// Email represent email identifier.
|
||||
type Email struct {
|
||||
user string
|
||||
host string
|
||||
}
|
||||
|
||||
var ErrEmailInvalid error = Error{
|
||||
Code: ErrorCodeInvalidRequest,
|
||||
Description: "cannot parse email",
|
||||
URI: "",
|
||||
State: "",
|
||||
frame: xerrors.Caller(1),
|
||||
}
|
||||
var ErrEmailInvalid error = NewError(ErrorCodeInvalidRequest, "cannot parse email")
|
||||
|
||||
// ParseEmail parse strings to email identifier.
|
||||
func ParseEmail(src string) (*Email, error) {
|
||||
parts := strings.Split(strings.TrimPrefix(src, "mailto:"), "@")
|
||||
if len(parts) != 2 { //nolint: gomnd
|
||||
|
@ -32,6 +26,7 @@ func ParseEmail(src string) (*Email, error) {
|
|||
}, nil
|
||||
}
|
||||
|
||||
// TestEmail returns valid random generated email identifier.
|
||||
func TestEmail(tb testing.TB) *Email {
|
||||
tb.Helper()
|
||||
|
||||
|
@ -41,6 +36,7 @@ func TestEmail(tb testing.TB) *Email {
|
|||
}
|
||||
}
|
||||
|
||||
// String returns string representation of email identifier.
|
||||
func (e Email) String() string {
|
||||
return e.user + "@" + e.host
|
||||
}
|
||||
|
|
|
@ -140,6 +140,10 @@ func (e Error) FormatError(p xerrors.Printer) error {
|
|||
p.Print(": ", e.Description)
|
||||
}
|
||||
|
||||
if !p.Detail() {
|
||||
return nil
|
||||
}
|
||||
|
||||
e.frame.Format(p)
|
||||
|
||||
return nil
|
||||
|
|
|
@ -10,31 +10,33 @@ import (
|
|||
// NOTE(toby3d): Encapsulate enums in structs for extra compile-time safety:
|
||||
// https://threedots.tech/post/safer-enums-in-go/#struct-based-enums
|
||||
type GrantType struct {
|
||||
slug string
|
||||
uid string
|
||||
}
|
||||
|
||||
//nolint: gochecknoglobals // NOTE(toby3d): structs cannot be constants
|
||||
var (
|
||||
GrantTypeUndefined = GrantType{slug: ""}
|
||||
GrantTypeAuthorizationCode = GrantType{slug: "authorization_code"}
|
||||
GrantTypeUndefined = GrantType{uid: ""}
|
||||
GrantTypeAuthorizationCode = GrantType{uid: "authorization_code"}
|
||||
|
||||
// TicketAuth extension.
|
||||
GrantTypeTicket = GrantType{slug: "ticket"}
|
||||
GrantTypeTicket = GrantType{uid: "ticket"}
|
||||
)
|
||||
|
||||
var ErrGrantTypeUnknown = errors.New("unknown grant type")
|
||||
var ErrGrantTypeUnknown error = errors.New("unknown grant type")
|
||||
|
||||
func ParseGrantType(slug string) (GrantType, error) {
|
||||
switch strings.ToLower(slug) {
|
||||
case GrantTypeAuthorizationCode.slug:
|
||||
// ParseGrantType parse grant_type value as GrantType struct enum.
|
||||
func ParseGrantType(uid string) (GrantType, error) {
|
||||
switch strings.ToLower(uid) {
|
||||
case GrantTypeAuthorizationCode.uid:
|
||||
return GrantTypeAuthorizationCode, nil
|
||||
case GrantTypeTicket.slug:
|
||||
case GrantTypeTicket.uid:
|
||||
return GrantTypeTicket, nil
|
||||
}
|
||||
|
||||
return GrantTypeUndefined, fmt.Errorf("%w: %s", ErrGrantTypeUnknown, slug)
|
||||
return GrantTypeUndefined, fmt.Errorf("%w: %s", ErrGrantTypeUnknown, uid)
|
||||
}
|
||||
|
||||
// UnmarshalForm implements custom unmarshler for form values.
|
||||
func (gt *GrantType) UnmarshalForm(src []byte) error {
|
||||
responseType, err := ParseGrantType(string(src))
|
||||
if err != nil {
|
||||
|
@ -46,6 +48,7 @@ func (gt *GrantType) UnmarshalForm(src []byte) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements custom unmarshler for JSON.
|
||||
func (gt *GrantType) UnmarshalJSON(v []byte) error {
|
||||
src, err := strconv.Unquote(string(v))
|
||||
if err != nil {
|
||||
|
@ -62,6 +65,7 @@ func (gt *GrantType) UnmarshalJSON(v []byte) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// String returns string representation of grant type.
|
||||
func (gt GrantType) String() string {
|
||||
return gt.slug
|
||||
return gt.uid
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ type Me struct {
|
|||
me *http.URI
|
||||
}
|
||||
|
||||
// ParseMe parse string as me URL identifier.
|
||||
//nolint: funlen
|
||||
func ParseMe(raw string) (*Me, error) {
|
||||
me := http.AcquireURI()
|
||||
|
@ -26,6 +27,7 @@ func ParseMe(raw string) (*Me, error) {
|
|||
Code: ErrorCodeInvalidRequest,
|
||||
Description: err.Error(),
|
||||
URI: "https://indieauth.net/source/#user-profile-url",
|
||||
State: "",
|
||||
frame: xerrors.Caller(1),
|
||||
}
|
||||
}
|
||||
|
@ -36,6 +38,7 @@ func ParseMe(raw string) (*Me, error) {
|
|||
Code: ErrorCodeInvalidRequest,
|
||||
Description: "profile URL MUST have either an https or http scheme",
|
||||
URI: "https://indieauth.net/source/#user-profile-url",
|
||||
State: "",
|
||||
frame: xerrors.Caller(1),
|
||||
}
|
||||
}
|
||||
|
@ -47,6 +50,7 @@ func ParseMe(raw string) (*Me, error) {
|
|||
Description: "profile URL MUST contain a path component (/ is a valid path), MUST NOT " +
|
||||
"contain single-dot or double-dot path segments",
|
||||
URI: "https://indieauth.net/source/#user-profile-url",
|
||||
State: "",
|
||||
frame: xerrors.Caller(1),
|
||||
}
|
||||
}
|
||||
|
@ -56,6 +60,7 @@ func ParseMe(raw string) (*Me, error) {
|
|||
Code: ErrorCodeInvalidRequest,
|
||||
Description: "profile URL MUST NOT contain a fragment component",
|
||||
URI: "https://indieauth.net/source/#user-profile-url",
|
||||
State: "",
|
||||
frame: xerrors.Caller(1),
|
||||
}
|
||||
}
|
||||
|
@ -65,6 +70,7 @@ func ParseMe(raw string) (*Me, error) {
|
|||
Code: ErrorCodeInvalidRequest,
|
||||
Description: "profile URL MUST NOT contain a username or password component",
|
||||
URI: "https://indieauth.net/source/#user-profile-url",
|
||||
State: "",
|
||||
frame: xerrors.Caller(1),
|
||||
}
|
||||
}
|
||||
|
@ -75,6 +81,7 @@ func ParseMe(raw string) (*Me, error) {
|
|||
Code: ErrorCodeInvalidRequest,
|
||||
Description: "profile host name MUST be a domain name",
|
||||
URI: "https://indieauth.net/source/#user-profile-url",
|
||||
State: "",
|
||||
frame: xerrors.Caller(1),
|
||||
}
|
||||
}
|
||||
|
@ -84,6 +91,7 @@ func ParseMe(raw string) (*Me, error) {
|
|||
Code: ErrorCodeInvalidRequest,
|
||||
Description: "profile MUST NOT contain a port",
|
||||
URI: "https://indieauth.net/source/#user-profile-url",
|
||||
State: "",
|
||||
frame: xerrors.Caller(1),
|
||||
}
|
||||
}
|
||||
|
@ -93,6 +101,7 @@ func ParseMe(raw string) (*Me, error) {
|
|||
Code: ErrorCodeInvalidRequest,
|
||||
Description: "profile MUST NOT be ipv4 or ipv6 addresses",
|
||||
URI: "https://indieauth.net/source/#user-profile-url",
|
||||
State: "",
|
||||
frame: xerrors.Caller(1),
|
||||
}
|
||||
}
|
||||
|
@ -100,7 +109,7 @@ func ParseMe(raw string) (*Me, error) {
|
|||
return &Me{me: me}, nil
|
||||
}
|
||||
|
||||
// TestMe returns a valid random generated Me for tests.
|
||||
// TestMe returns valid random generated me for tests.
|
||||
func TestMe(tb testing.TB, src string) *Me {
|
||||
tb.Helper()
|
||||
|
||||
|
@ -110,7 +119,7 @@ func TestMe(tb testing.TB, src string) *Me {
|
|||
return me
|
||||
}
|
||||
|
||||
// UnmarshalForm parses the value of the form key into the Me domain.
|
||||
// UnmarshalForm implements custom unmarshler for form values.
|
||||
func (m *Me) UnmarshalForm(v []byte) error {
|
||||
me, err := ParseMe(string(v))
|
||||
if err != nil {
|
||||
|
@ -122,15 +131,16 @@ func (m *Me) UnmarshalForm(v []byte) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements custom unmarshler for JSON.
|
||||
func (m *Me) UnmarshalJSON(v []byte) error {
|
||||
src, err := strconv.Unquote(string(v))
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("UnmarshalJSON: %w", err)
|
||||
}
|
||||
|
||||
me, err := ParseMe(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("UnmarshalForm: %w", err)
|
||||
return fmt.Errorf("UnmarshalJSON: %w", err)
|
||||
}
|
||||
|
||||
*m = *me
|
||||
|
@ -138,11 +148,12 @@ func (m *Me) UnmarshalJSON(v []byte) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON implements custom marshler for JSON.
|
||||
func (m Me) MarshalJSON() ([]byte, error) {
|
||||
return []byte(strconv.Quote(m.String())), nil
|
||||
}
|
||||
|
||||
// URI returns copy of parsed Me in *fasthttp.URI representation.
|
||||
// URI returns copy of parsed me in *fasthttp.URI representation.
|
||||
// This copy MUST be released via fasthttp.ReleaseURI.
|
||||
func (m Me) URI() *http.URI {
|
||||
if m.me == nil {
|
||||
|
@ -155,7 +166,7 @@ func (m Me) URI() *http.URI {
|
|||
return u
|
||||
}
|
||||
|
||||
// URL returns copy of parsed Me in *url.URL representation.
|
||||
// URL returns copy of parsed me in *url.URL representation.
|
||||
func (m Me) URL() *url.URL {
|
||||
if m.me == nil {
|
||||
return nil
|
||||
|
@ -171,7 +182,7 @@ func (m Me) URL() *url.URL {
|
|||
}
|
||||
}
|
||||
|
||||
// String returns string representation of Me.
|
||||
// String returns string representation of me.
|
||||
func (m Me) String() string {
|
||||
if m.me == nil {
|
||||
return ""
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
package domain
|
||||
|
||||
import (
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
http "github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
// Provider represent 3rd party RelMeAuth provider.
|
||||
type Provider struct {
|
||||
Scopes []string
|
||||
AuthURL string
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
Name string
|
||||
Photo string
|
||||
RedirectURL string
|
||||
TokenURL string
|
||||
UID string
|
||||
URL string
|
||||
}
|
||||
|
||||
//nolint: gochecknoglobals
|
||||
var (
|
||||
DefaultProviderDirect = Provider{
|
||||
AuthURL: "/authorize",
|
||||
Name: "IndieAuth",
|
||||
Photo: path.Join("static", "icon.svg"),
|
||||
RedirectURL: path.Join("callback"),
|
||||
Scopes: []string{},
|
||||
TokenURL: "/token",
|
||||
UID: "direct",
|
||||
URL: "/",
|
||||
}
|
||||
|
||||
DefaultProviderTwitter = Provider{
|
||||
AuthURL: "https://twitter.com/i/oauth2/authorize",
|
||||
Name: "Twitter",
|
||||
Photo: path.Join("static", "providers", "twitter.svg"),
|
||||
RedirectURL: path.Join("callback", "twitter"),
|
||||
Scopes: []string{
|
||||
"tweet.read",
|
||||
"users.read",
|
||||
},
|
||||
TokenURL: "https://api.twitter.com/2/oauth2/token",
|
||||
UID: "twitter",
|
||||
URL: "https://twitter.com/",
|
||||
}
|
||||
|
||||
DefaultProviderGitHub = Provider{
|
||||
AuthURL: "https://github.com/login/oauth/authorize",
|
||||
Name: "GitHub",
|
||||
Photo: path.Join("static", "providers", "github.svg"),
|
||||
RedirectURL: path.Join("callback", "github"),
|
||||
Scopes: []string{
|
||||
"read:user",
|
||||
"user:email",
|
||||
},
|
||||
TokenURL: "https://github.com/login/oauth/access_token",
|
||||
UID: "github",
|
||||
URL: "https://github.com/",
|
||||
}
|
||||
|
||||
DefaultProviderGitLab = Provider{
|
||||
AuthURL: "https://gitlab.com/oauth/authorize",
|
||||
Name: "GitLab",
|
||||
Photo: path.Join("static", "providers", "gitlab.svg"),
|
||||
RedirectURL: path.Join("callback", "gitlab"),
|
||||
Scopes: []string{
|
||||
"read_user",
|
||||
},
|
||||
TokenURL: "https://gitlab.com/oauth/token",
|
||||
UID: "gitlab",
|
||||
URL: "https://gitlab.com/",
|
||||
}
|
||||
|
||||
DefaultProviderMastodon = Provider{
|
||||
AuthURL: "https://mstdn.io/oauth/authorize",
|
||||
Name: "Mastodon",
|
||||
Photo: path.Join("static", "providers", "mastodon.svg"),
|
||||
RedirectURL: path.Join("callback", "mastodon"),
|
||||
Scopes: []string{
|
||||
"read:accounts",
|
||||
},
|
||||
TokenURL: "https://mstdn.io/oauth/token",
|
||||
UID: "mastodon",
|
||||
URL: "https://mstdn.io/",
|
||||
}
|
||||
)
|
||||
|
||||
// AuthCodeURL returns URL for authorize user in RelMeAuth client.
|
||||
func (p Provider) AuthCodeURL(state string) *URL {
|
||||
u := http.AcquireURI()
|
||||
u.Update(p.AuthURL)
|
||||
|
||||
for k, v := range map[string]string{
|
||||
"client_id": p.ClientID,
|
||||
"redirect_uri": p.RedirectURL,
|
||||
"response_type": "code",
|
||||
"scope": strings.Join(p.Scopes, " "),
|
||||
"state": state,
|
||||
} {
|
||||
u.QueryArgs().Set(k, v)
|
||||
}
|
||||
|
||||
return &URL{URI: u}
|
||||
}
|
|
@ -10,42 +10,44 @@ import (
|
|||
// NOTE(toby3d): Encapsulate enums in structs for extra compile-time safety:
|
||||
// https://threedots.tech/post/safer-enums-in-go/#struct-based-enums
|
||||
type ResponseType struct {
|
||||
slug string
|
||||
uid string
|
||||
}
|
||||
|
||||
//nolint: gochecknoglobals // NOTE(toby3d): structs cannot be constants
|
||||
var (
|
||||
ResponseTypeUndefined ResponseType = ResponseType{slug: ""}
|
||||
ResponseTypeUndefined ResponseType = ResponseType{uid: ""}
|
||||
|
||||
// Deprecated(toby3d): Only accept response_type=code requests, and for
|
||||
// backwards-compatible support, treat response_type=id requests as
|
||||
// response_type=code requests:
|
||||
// https://aaronparecki.com/2020/12/03/1/indieauth-2020#response-type
|
||||
ResponseTypeID ResponseType = ResponseType{slug: "id"}
|
||||
ResponseTypeID ResponseType = ResponseType{uid: "id"}
|
||||
|
||||
// Indicates to the authorization server that an authorization code
|
||||
// should be returned as the response:
|
||||
// https://indieauth.net/source/#authorization-request-li-1
|
||||
ResponseTypeCode ResponseType = ResponseType{slug: "code"}
|
||||
ResponseTypeCode ResponseType = ResponseType{uid: "code"}
|
||||
)
|
||||
|
||||
var ErrResponseTypeUnknown = errors.New("unknown grant type")
|
||||
var ErrResponseTypeUnknown error = errors.New("unknown grant type")
|
||||
|
||||
func ParseResponseType(slug string) (ResponseType, error) {
|
||||
switch strings.ToLower(slug) {
|
||||
case ResponseTypeCode.slug:
|
||||
// ParseResponseType parse string as response type struct enum.
|
||||
func ParseResponseType(uid string) (ResponseType, error) {
|
||||
switch strings.ToLower(uid) {
|
||||
case ResponseTypeCode.uid:
|
||||
return ResponseTypeCode, nil
|
||||
case ResponseTypeID.slug:
|
||||
case ResponseTypeID.uid:
|
||||
return ResponseTypeID, nil
|
||||
}
|
||||
|
||||
return ResponseTypeUndefined, fmt.Errorf("%w: %s", ErrResponseTypeUnknown, slug)
|
||||
return ResponseTypeUndefined, fmt.Errorf("%w: %s", ErrResponseTypeUnknown, uid)
|
||||
}
|
||||
|
||||
// UnmarshalForm implements custom unmarshler for form values.
|
||||
func (rt *ResponseType) UnmarshalForm(src []byte) error {
|
||||
responseType, err := ParseResponseType(string(src))
|
||||
if err != nil {
|
||||
return fmt.Errorf("response_type: %w", err)
|
||||
return fmt.Errorf("UnmarshalForm: %w", err)
|
||||
}
|
||||
|
||||
*rt = responseType
|
||||
|
@ -53,15 +55,16 @@ func (rt *ResponseType) UnmarshalForm(src []byte) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements custom unmarshler for JSON.
|
||||
func (rt *ResponseType) UnmarshalJSON(v []byte) error {
|
||||
src, err := strconv.Unquote(string(v))
|
||||
uid, err := strconv.Unquote(string(v))
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("UnmarshalJSON: %w", err)
|
||||
}
|
||||
|
||||
responseType, err := ParseResponseType(string(src))
|
||||
responseType, err := ParseResponseType(string(uid))
|
||||
if err != nil {
|
||||
return fmt.Errorf("response_type: %w", err)
|
||||
return fmt.Errorf("UnmarshalJSON: %w", err)
|
||||
}
|
||||
|
||||
*rt = responseType
|
||||
|
@ -69,6 +72,7 @@ func (rt *ResponseType) UnmarshalJSON(v []byte) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// String returns string representation of response type.
|
||||
func (rt ResponseType) String() string {
|
||||
return rt.slug
|
||||
return rt.uid
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package domain
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
@ -9,40 +8,43 @@ import (
|
|||
)
|
||||
|
||||
type (
|
||||
// NOTE(toby3d): https://threedots.tech/post/safer-enums-in-go/#struct-based-enums
|
||||
// Scope represent single token scope supported by IndieAuth.
|
||||
//
|
||||
// NOTE(toby3d): Encapsulate enums in structs for extra compile-time safety:
|
||||
// https://threedots.tech/post/safer-enums-in-go/#struct-based-enums
|
||||
Scope struct {
|
||||
slug string
|
||||
uid string
|
||||
}
|
||||
|
||||
// Scopes represent set of Scope domains.
|
||||
Scopes []Scope
|
||||
)
|
||||
|
||||
var ErrScopeUnknown = errors.New("unknown scope")
|
||||
var ErrScopeUnknown error = NewError(ErrorCodeInvalidRequest, "unknown scope")
|
||||
|
||||
//nolint: gochecknoglobals // NOTE(toby3d): structs cannot be constants
|
||||
var (
|
||||
ScopeUndefined = Scope{slug: ""}
|
||||
ScopeUndefined = Scope{uid: ""}
|
||||
|
||||
// https://indieweb.org/scope#Micropub_Scopes
|
||||
ScopeCreate = Scope{slug: "create"}
|
||||
ScopeDelete = Scope{slug: "delete"}
|
||||
ScopeDraft = Scope{slug: "draft"}
|
||||
ScopeMedia = Scope{slug: "media"}
|
||||
ScopeUpdate = Scope{slug: "update"}
|
||||
ScopeCreate = Scope{uid: "create"}
|
||||
ScopeDelete = Scope{uid: "delete"}
|
||||
ScopeDraft = Scope{uid: "draft"}
|
||||
ScopeMedia = Scope{uid: "media"}
|
||||
ScopeUpdate = Scope{uid: "update"}
|
||||
|
||||
// https://indieweb.org/scope#Microsub_Scopes
|
||||
ScopeBlock = Scope{slug: "block"}
|
||||
ScopeChannels = Scope{slug: "channels"}
|
||||
ScopeFollow = Scope{slug: "follow"}
|
||||
ScopeMute = Scope{slug: "mute"}
|
||||
ScopeRead = Scope{slug: "read"}
|
||||
ScopeBlock = Scope{uid: "block"}
|
||||
ScopeChannels = Scope{uid: "channels"}
|
||||
ScopeFollow = Scope{uid: "follow"}
|
||||
ScopeMute = Scope{uid: "mute"}
|
||||
ScopeRead = Scope{uid: "read"}
|
||||
|
||||
// This scope requests access to the user's default profile information
|
||||
// which include the following properties: name, `photo, url.
|
||||
//
|
||||
// NOTE(toby3d): https://indieauth.net/source/#profile-information
|
||||
ScopeProfile = Scope{slug: "profile"}
|
||||
ScopeProfile = Scope{uid: "profile"}
|
||||
|
||||
// This scope requests access to the user's email address in the
|
||||
// following property: email.
|
||||
|
@ -52,45 +54,49 @@ var (
|
|||
// and must be requested along with the profile scope if desired.
|
||||
//
|
||||
// NOTE(toby3d): https://indieauth.net/source/#profile-information
|
||||
ScopeEmail = Scope{slug: "email"}
|
||||
ScopeEmail = Scope{uid: "email"}
|
||||
)
|
||||
|
||||
//nolint: gochecknoglobals // NOTE(toby3d): maps cannot be constants
|
||||
var slugsScopes = map[string]Scope{
|
||||
ScopeBlock.slug: ScopeBlock,
|
||||
ScopeChannels.slug: ScopeChannels,
|
||||
ScopeCreate.slug: ScopeCreate,
|
||||
ScopeDelete.slug: ScopeDelete,
|
||||
ScopeDraft.slug: ScopeDraft,
|
||||
ScopeEmail.slug: ScopeEmail,
|
||||
ScopeFollow.slug: ScopeFollow,
|
||||
ScopeMedia.slug: ScopeMedia,
|
||||
ScopeMute.slug: ScopeMute,
|
||||
ScopeProfile.slug: ScopeProfile,
|
||||
ScopeRead.slug: ScopeRead,
|
||||
ScopeUpdate.slug: ScopeUpdate,
|
||||
var uidsScopes = map[string]Scope{
|
||||
ScopeBlock.uid: ScopeBlock,
|
||||
ScopeChannels.uid: ScopeChannels,
|
||||
ScopeCreate.uid: ScopeCreate,
|
||||
ScopeDelete.uid: ScopeDelete,
|
||||
ScopeDraft.uid: ScopeDraft,
|
||||
ScopeEmail.uid: ScopeEmail,
|
||||
ScopeFollow.uid: ScopeFollow,
|
||||
ScopeMedia.uid: ScopeMedia,
|
||||
ScopeMute.uid: ScopeMute,
|
||||
ScopeProfile.uid: ScopeProfile,
|
||||
ScopeRead.uid: ScopeRead,
|
||||
ScopeUpdate.uid: ScopeUpdate,
|
||||
}
|
||||
|
||||
// ParseScope parses scope slug into Scope domain.
|
||||
func ParseScope(slug string) (Scope, error) {
|
||||
if scope, ok := slugsScopes[strings.ToLower(slug)]; ok {
|
||||
func ParseScope(uid string) (Scope, error) {
|
||||
if scope, ok := uidsScopes[strings.ToLower(uid)]; ok {
|
||||
return scope, nil
|
||||
}
|
||||
|
||||
return ScopeUndefined, fmt.Errorf("%w: %s", ErrScopeUnknown, slug)
|
||||
return ScopeUndefined, fmt.Errorf("%w: %s", ErrScopeUnknown, uid)
|
||||
}
|
||||
|
||||
// UnmarshalForm parses the value of the form key into the Scope domain.
|
||||
// UnmarshalForm implements custom unmarshler for form values.
|
||||
func (s *Scopes) UnmarshalForm(v []byte) error {
|
||||
scopes := make(Scopes, 0)
|
||||
|
||||
for _, rawScope := range strings.Fields(string(v)) {
|
||||
scope, err := ParseScope(rawScope)
|
||||
if err != nil {
|
||||
return fmt.Errorf("scopes: %w", err)
|
||||
return fmt.Errorf("UnmarshalForm: %w", err)
|
||||
}
|
||||
|
||||
*s = append(scopes, scope)
|
||||
if scopes.Has(scope) {
|
||||
continue
|
||||
}
|
||||
|
||||
scopes = append(scopes, scope)
|
||||
}
|
||||
|
||||
*s = scopes
|
||||
|
@ -98,18 +104,23 @@ func (s *Scopes) UnmarshalForm(v []byte) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements custom unmarshler for JSON.
|
||||
func (s *Scopes) UnmarshalJSON(v []byte) error {
|
||||
src, err := strconv.Unquote(string(v))
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("UnmarshalJSON: %w", err)
|
||||
}
|
||||
|
||||
result := make([]Scope, 0)
|
||||
result := make(Scopes, 0)
|
||||
|
||||
for _, scope := range strings.Fields(src) {
|
||||
s, err := ParseScope(scope)
|
||||
if err != nil {
|
||||
return fmt.Errorf("scope: %w", err)
|
||||
return fmt.Errorf("UnmarshalJSON: %w", err)
|
||||
}
|
||||
|
||||
if result.Has(s) {
|
||||
continue
|
||||
}
|
||||
|
||||
result = append(result, s)
|
||||
|
@ -120,6 +131,7 @@ func (s *Scopes) UnmarshalJSON(v []byte) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements custom marshler for JSON.
|
||||
func (s Scopes) MarshalJSON() ([]byte, error) {
|
||||
scopes := make([]string, len(s))
|
||||
|
||||
|
@ -132,11 +144,12 @@ func (s Scopes) MarshalJSON() ([]byte, error) {
|
|||
return []byte(strconv.Quote(strings.Join(scopes, " "))), nil
|
||||
}
|
||||
|
||||
// String returns scope slug as string.
|
||||
// String returns string representation of scope.
|
||||
func (s Scope) String() string {
|
||||
return s.slug
|
||||
return s.uid
|
||||
}
|
||||
|
||||
// String returns string representation of scopes.
|
||||
func (s Scopes) String() string {
|
||||
scopes := make([]string, len(s))
|
||||
|
||||
|
@ -146,3 +159,29 @@ func (s Scopes) String() string {
|
|||
|
||||
return strings.Join(scopes, " ")
|
||||
}
|
||||
|
||||
// IsEmpty returns true if the set does not contain valid scope.
|
||||
func (s Scopes) IsEmpty() bool {
|
||||
for i := range s {
|
||||
if s[i] == ScopeUndefined {
|
||||
continue
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Has check what input scope contains in current scopes collection.
|
||||
func (s Scopes) Has(scope Scope) bool {
|
||||
for i := range s {
|
||||
if s[i] != scope {
|
||||
continue
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -1,27 +1,41 @@
|
|||
package domain
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"source.toby3d.me/website/indieauth/internal/random"
|
||||
)
|
||||
|
||||
type Session struct {
|
||||
ClientID *ClientID
|
||||
Me *Me
|
||||
RedirectURI *URL
|
||||
Profile *Profile
|
||||
CodeChallengeMethod CodeChallengeMethod
|
||||
Scope Scopes
|
||||
Code string
|
||||
CodeChallenge string
|
||||
}
|
||||
|
||||
// TestSession returns valid random generated session for tests.
|
||||
func TestSession(tb testing.TB) *Session {
|
||||
tb.Helper()
|
||||
|
||||
code, err := random.String(24)
|
||||
require.NoError(tb, err)
|
||||
|
||||
return &Session{
|
||||
ClientID: TestClientID(tb),
|
||||
Code: code,
|
||||
CodeChallenge: "hackme",
|
||||
CodeChallengeMethod: CodeChallengeMethodPLAIN,
|
||||
Me: TestMe(tb, "https://user.example.net/"),
|
||||
RedirectURI: TestURL(tb, "https://example.com/callback"),
|
||||
CodeChallengeMethod: CodeChallengeMethodPLAIN,
|
||||
Scope: Scopes{ScopeProfile, ScopeEmail},
|
||||
Code: "abcdefg",
|
||||
CodeChallenge: "hackme",
|
||||
Scope: Scopes{
|
||||
ScopeEmail,
|
||||
ScopeProfile,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ type Ticket struct {
|
|||
Subject *Me
|
||||
}
|
||||
|
||||
// TestTicket returns valid random generated ticket for tests.
|
||||
func TestTicket(tb testing.TB) *Ticket {
|
||||
tb.Helper()
|
||||
|
||||
|
|
|
@ -16,28 +16,31 @@ import (
|
|||
type (
|
||||
// Token describes the data of the token used by the clients.
|
||||
Token struct {
|
||||
AccessToken string
|
||||
Scope Scopes
|
||||
ClientID *ClientID
|
||||
Me *Me
|
||||
Scope Scopes
|
||||
AccessToken string
|
||||
}
|
||||
|
||||
// NewTokenOptions contains options for NewToken function.
|
||||
NewTokenOptions struct {
|
||||
Algorithm string
|
||||
Expiration time.Duration
|
||||
Issuer *ClientID
|
||||
NonceLength int
|
||||
Scope Scopes
|
||||
Secret []byte
|
||||
Issuer *ClientID
|
||||
Subject *Me
|
||||
Secret []byte
|
||||
Algorithm string
|
||||
NonceLength int
|
||||
}
|
||||
)
|
||||
|
||||
//nolint: gochecknoglobals
|
||||
var DefaultNewTokenOptions = NewTokenOptions{
|
||||
Algorithm: "HS256",
|
||||
NonceLength: 32,
|
||||
}
|
||||
|
||||
// NewToken create a new token by provided options.
|
||||
func NewToken(opts NewTokenOptions) (*Token, error) {
|
||||
if opts.NonceLength == 0 {
|
||||
opts.NonceLength = DefaultNewTokenOptions.NonceLength
|
||||
|
@ -55,13 +58,16 @@ func NewToken(opts NewTokenOptions) (*Token, error) {
|
|||
}
|
||||
|
||||
t := jwt.New()
|
||||
t.Set(jwt.IssuerKey, opts.Issuer.String())
|
||||
t.Set(jwt.SubjectKey, opts.Subject.String())
|
||||
t.Set(jwt.NotBeforeKey, now)
|
||||
t.Set(jwt.IssuedAtKey, now)
|
||||
t.Set("scope", opts.Scope)
|
||||
t.Set("nonce", nonce)
|
||||
|
||||
if opts.Issuer != nil {
|
||||
t.Set(jwt.IssuerKey, opts.Issuer.String())
|
||||
}
|
||||
|
||||
if opts.Expiration != 0 {
|
||||
t.Set(jwt.ExpirationKey, now.Add(opts.Expiration))
|
||||
}
|
||||
|
@ -79,7 +85,7 @@ func NewToken(opts NewTokenOptions) (*Token, error) {
|
|||
}, err
|
||||
}
|
||||
|
||||
// TestToken returns a valid Token with the generated test data filled in.
|
||||
// TestToken returns valid random generated token for tests.
|
||||
func TestToken(tb testing.TB) *Token {
|
||||
tb.Helper()
|
||||
|
||||
|
@ -129,6 +135,7 @@ func (t Token) SetAuthHeader(r *http.Request) {
|
|||
r.Header.Set(http.HeaderAuthorization, t.String())
|
||||
}
|
||||
|
||||
// String returns string representation of token.
|
||||
func (t Token) String() string {
|
||||
if t.AccessToken == "" {
|
||||
return ""
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package domain
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
@ -8,19 +9,22 @@ import (
|
|||
http "github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
// URL describe any valid HTTP URL.
|
||||
type URL struct {
|
||||
*http.URI
|
||||
}
|
||||
|
||||
// ParseURL parse strings as URL.
|
||||
func ParseURL(src string) (*URL, error) {
|
||||
u := http.AcquireURI()
|
||||
if err := u.Parse(nil, []byte(src)); err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("cannot parse URL: %w", err)
|
||||
}
|
||||
|
||||
return &URL{URI: u}, nil
|
||||
}
|
||||
|
||||
// TestURL returns URL of provided input for tests.
|
||||
func TestURL(tb testing.TB, src string) *URL {
|
||||
tb.Helper()
|
||||
|
||||
|
@ -32,10 +36,11 @@ func TestURL(tb testing.TB, src string) *URL {
|
|||
}
|
||||
}
|
||||
|
||||
// UnmarshalForm implements custom unmarshler for form values.
|
||||
func (u *URL) UnmarshalForm(v []byte) error {
|
||||
url, err := ParseURL(string(v))
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("UnmarshalForm: %w", err)
|
||||
}
|
||||
|
||||
*u = *url
|
||||
|
@ -43,15 +48,16 @@ func (u *URL) UnmarshalForm(v []byte) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements custom unmarshler for JSON.
|
||||
func (u *URL) UnmarshalJSON(v []byte) error {
|
||||
src, err := strconv.Unquote(string(v))
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("UnmarshalJSON: %w", err)
|
||||
}
|
||||
|
||||
url, err := ParseURL(src)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("UnmarshalJSON: %w", err)
|
||||
}
|
||||
|
||||
*u = *url
|
||||
|
@ -59,6 +65,7 @@ func (u *URL) UnmarshalJSON(v []byte) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// URL returns url.URL representation of URL.
|
||||
func (u URL) URL() *url.URL {
|
||||
if u.URI == nil {
|
||||
return nil
|
||||
|
|
|
@ -15,6 +15,7 @@ type User struct {
|
|||
*Profile
|
||||
}
|
||||
|
||||
// TestUser returns valid random generated user for tests.
|
||||
func TestUser(tb testing.TB) *User {
|
||||
tb.Helper()
|
||||
|
||||
|
|
Loading…
Reference in New Issue