🎨 Format domain package code

This commit is contained in:
Maxim Lebedev 2022-01-29 22:50:45 +05:00
parent f6174c67e0
commit 14f4d7d2ef
Signed by: toby3d
GPG Key ID: 1F14E25B7C119FC5
17 changed files with 401 additions and 155 deletions

View File

@ -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
} }

View File

@ -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

View File

@ -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()),

View File

@ -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
} }

View File

@ -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,

View File

@ -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
} }

View File

@ -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

View File

@ -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
} }

View File

@ -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 ""

108
internal/domain/provider.go Normal file
View File

@ -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}
}

View File

@ -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
} }

View File

@ -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
}

View File

@ -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", },
} }
} }

View File

@ -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()

View File

@ -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 ""

View File

@ -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

View File

@ -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()