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