🎨 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
import (
"errors"
"fmt"
"strconv"
"strings"
)
// Action represent action for token endpoint supported by IndieAuth.
//
// NOTE(toby3d): Encapsulate enums in structs for extra compile-time safety:
// https://threedots.tech/post/safer-enums-in-go/#struct-based-enums
type Action struct {
slug string
uid string
}
//nolint: gochecknoglobals // NOTE(toby3d): structs cannot be constants
var (
ActionUndefined = Action{slug: ""}
ActionRevoke = Action{slug: "revoke"}
ActionTicket = Action{slug: "ticket"}
ActionUndefined = Action{uid: ""}
// ActionRevoke represent action for revoke token.
ActionRevoke = Action{uid: "revoke"}
// ActionTicket represent action for TicketAuth extension.
ActionTicket = Action{uid: "ticket"}
)
var ErrActionUnknown = errors.New("unknown action method")
var ErrActionUnknown error = NewError(ErrorCodeInvalidRequest, "unknown action method")
// ParseAction parse string identifier of action into struct enum.
func ParseAction(slug string) (Action, error) {
switch strings.ToLower(slug) {
case ActionRevoke.slug:
func ParseAction(uid string) (Action, error) {
switch strings.ToLower(uid) {
case ActionRevoke.uid:
return ActionRevoke, nil
case ActionTicket.slug:
case ActionTicket.uid:
return ActionTicket, nil
}
return ActionUndefined, fmt.Errorf("%w: %s", ErrActionUnknown, slug)
return ActionUndefined, fmt.Errorf("%w: %s", ErrActionUnknown, uid)
}
// UnmarshalForm implements custom unmarshler for form values.
func (a *Action) UnmarshalForm(v []byte) error {
action, err := ParseAction(string(v))
if err != nil {
return fmt.Errorf("action: %w", err)
return fmt.Errorf("UnmarshalForm: %w", err)
}
*a = action
@ -46,15 +51,16 @@ func (a *Action) UnmarshalForm(v []byte) error {
return nil
}
// UnmarshalJSON implements custom unmarshler for JSON.
func (a *Action) UnmarshalJSON(v []byte) error {
src, err := strconv.Unquote(string(v))
if err != nil {
return err
return fmt.Errorf("UnmarshalJSON: %w", err)
}
action, err := ParseAction(src)
if err != nil {
return fmt.Errorf("action: %w", err)
return fmt.Errorf("UnmarshalJSON: %w", err)
}
*a = action
@ -64,5 +70,5 @@ func (a *Action) UnmarshalJSON(v []byte) error {
// String returns string representation of action.
func (a Action) String() string {
return a.slug
return a.uid
}

View File

@ -16,7 +16,18 @@ type Client struct {
Name []string
}
// TestClient returns a valid Client with the generated test data filled in.
// NewClient creates a new empty Client with provided ClientID, if any.
func NewClient(cid *ClientID) *Client {
return &Client{
ID: cid,
Logo: make([]*URL, 0),
RedirectURI: make([]*URL, 0),
URL: make([]*URL, 0),
Name: make([]string, 0),
}
}
// TestClient returns valid random generated client for tests.
func TestClient(tb testing.TB) *Client {
tb.Helper()
@ -76,6 +87,7 @@ func (c *Client) ValidateRedirectURI(redirectURI *URL) bool {
return false
}
// GetName safe returns first name, if any.
func (c Client) GetName() string {
if len(c.Name) < 1 {
return ""
@ -84,6 +96,7 @@ func (c Client) GetName() string {
return c.Name[0]
}
// GetURL safe returns first uRL, if any.
func (c Client) GetURL() *URL {
if len(c.URL) < 1 {
return nil
@ -92,6 +105,7 @@ func (c Client) GetURL() *URL {
return c.URL[0]
}
// GetLogo safe returns first logo, if any.
func (c Client) GetLogo() *URL {
if len(c.Logo) < 1 {
return nil

View File

@ -25,63 +25,70 @@ var (
localhostIPv6 = netaddr.MustParseIP("::1")
)
// ParseClientID parse string as client ID URL identifier.
//nolint: funlen
func ParseClientID(raw string) (*ClientID, error) {
clientID := http.AcquireURI()
if err := clientID.Parse(nil, []byte(raw)); err != nil {
func ParseClientID(src string) (*ClientID, error) {
cid := http.AcquireURI()
if err := cid.Parse(nil, []byte(src)); err != nil {
return nil, Error{
Code: ErrorCodeInvalidRequest,
Description: err.Error(),
URI: "https://indieauth.net/source/#client-identifier",
State: "",
frame: xerrors.Caller(1),
}
}
scheme := string(clientID.Scheme())
scheme := string(cid.Scheme())
if scheme != "http" && scheme != "https" {
return nil, Error{
Code: ErrorCodeInvalidRequest,
Description: "client identifier URL MUST have either an https or http scheme",
URI: "https://indieauth.net/source/#client-identifier",
State: "",
frame: xerrors.Caller(1),
}
}
path := string(clientID.PathOriginal())
path := string(cid.PathOriginal())
if path == "" || strings.Contains(path, "/.") || strings.Contains(path, "/..") {
return nil, Error{
Code: ErrorCodeInvalidRequest,
Description: "client identifier URL MUST contain a path component and MUST NOT contain " +
"single-dot or double-dot path segments",
URI: "https://indieauth.net/source/#client-identifier",
State: "",
frame: xerrors.Caller(1),
}
}
if clientID.Hash() != nil {
if cid.Hash() != nil {
return nil, Error{
Code: ErrorCodeInvalidRequest,
Description: "client identifier URL MUST NOT contain a fragment component",
URI: "https://indieauth.net/source/#client-identifier",
State: "",
frame: xerrors.Caller(1),
}
}
if clientID.Username() != nil || clientID.Password() != nil {
if cid.Username() != nil || cid.Password() != nil {
return nil, Error{
Code: ErrorCodeInvalidRequest,
Description: "client identifier URL MUST NOT contain a username or password component",
URI: "https://indieauth.net/source/#client-identifier",
State: "",
frame: xerrors.Caller(1),
}
}
domain := string(clientID.Host())
domain := string(cid.Host())
if domain == "" {
return nil, Error{
Code: ErrorCodeInvalidRequest,
Description: "client host name MUST be domain name or a loopback interface",
URI: "https://indieauth.net/source/#client-identifier",
State: "",
frame: xerrors.Caller(1),
}
}
@ -90,7 +97,9 @@ func ParseClientID(raw string) (*ClientID, error) {
if err != nil {
ipPort, err := netaddr.ParseIPPort(domain)
if err != nil {
return &ClientID{clientID: clientID}, nil
return &ClientID{
clientID: cid,
}, nil
}
ip = ipPort.IP()
@ -102,14 +111,17 @@ func ParseClientID(raw string) (*ClientID, error) {
Description: "client identifier URL MUST NOT be IPv4 or IPv6 addresses except for IPv4 " +
"127.0.0.1 or IPv6 [::1]",
URI: "https://indieauth.net/source/#client-identifier",
State: "",
frame: xerrors.Caller(1),
}
}
return &ClientID{clientID: clientID}, nil
return &ClientID{
clientID: cid,
}, nil
}
// TestClientID returns a valid random generated ClientID for tests.
// TestClientID returns valid random generated ClientID for tests.
func TestClientID(tb testing.TB) *ClientID {
tb.Helper()
@ -119,7 +131,7 @@ func TestClientID(tb testing.TB) *ClientID {
return clientID
}
// UnmarshalForm implements a custom form.Unmarshaler.
// UnmarshalForm implements custom unmarshler for form values.
func (cid *ClientID) UnmarshalForm(v []byte) error {
clientID, err := ParseClientID(string(v))
if err != nil {
@ -131,10 +143,11 @@ func (cid *ClientID) UnmarshalForm(v []byte) error {
return nil
}
// UnmarshalJSON implements custom unmarshler for JSON.
func (cid *ClientID) UnmarshalJSON(v []byte) error {
src, err := strconv.Unquote(string(v))
if err != nil {
return err
return fmt.Errorf("UnmarshalJSON: %w", err)
}
clientID, err := ParseClientID(src)
@ -147,6 +160,7 @@ func (cid *ClientID) UnmarshalJSON(v []byte) error {
return nil
}
// MarshalForm implements custom marshler for JSON.
func (cid ClientID) MarshalJSON() ([]byte, error) {
return []byte(strconv.Quote(cid.String())), nil
}
@ -160,6 +174,7 @@ func (cid ClientID) URI() *http.URI {
return u
}
// URL returns url.URL representation of client ID.
func (cid ClientID) URL() *url.URL {
return &url.URL{
Scheme: string(cid.clientID.Scheme()),

View File

@ -6,79 +6,80 @@ import (
"crypto/sha256"
"crypto/sha512"
"encoding/base64"
"errors"
"fmt"
"hash"
"strconv"
"strings"
)
// CodeChallengeMethod represent a PKCE challenge method for validate verifier.
//
// NOTE(toby3d): Encapsulate enums in structs for extra compile-time safety:
// https://threedots.tech/post/safer-enums-in-go/#struct-based-enums
type CodeChallengeMethod struct {
hash hash.Hash
slug string
uid string
}
//nolint: gochecknoglobals // NOTE(toby3d): structs cannot be constants
var (
CodeChallengeMethodUndefined = CodeChallengeMethod{
slug: "",
uid: "",
hash: nil,
}
CodeChallengeMethodPLAIN = CodeChallengeMethod{
slug: "PLAIN",
uid: "PLAIN",
hash: nil,
}
CodeChallengeMethodMD5 = CodeChallengeMethod{
slug: "MD5",
uid: "MD5",
hash: md5.New(),
}
CodeChallengeMethodS1 = CodeChallengeMethod{
slug: "S1",
uid: "S1",
hash: sha1.New(),
}
CodeChallengeMethodS256 = CodeChallengeMethod{
slug: "S256",
uid: "S256",
hash: sha256.New(),
}
CodeChallengeMethodS512 = CodeChallengeMethod{
slug: "S512",
uid: "S512",
hash: sha512.New(),
}
)
var ErrCodeChallengeMethodUnknown = errors.New("unknown code challenge method")
var ErrCodeChallengeMethodUnknown error = NewError(ErrorCodeInvalidRequest, "unknown code_challene_method")
//nolint: gochecknoglobals // NOTE(toby3d): maps cannot be constants
var slugsMethods = map[string]CodeChallengeMethod{
CodeChallengeMethodMD5.slug: CodeChallengeMethodMD5,
CodeChallengeMethodPLAIN.slug: CodeChallengeMethodPLAIN,
CodeChallengeMethodS1.slug: CodeChallengeMethodS1,
CodeChallengeMethodS256.slug: CodeChallengeMethodS256,
CodeChallengeMethodS512.slug: CodeChallengeMethodS512,
CodeChallengeMethodMD5.uid: CodeChallengeMethodMD5,
CodeChallengeMethodPLAIN.uid: CodeChallengeMethodPLAIN,
CodeChallengeMethodS1.uid: CodeChallengeMethodS1,
CodeChallengeMethodS256.uid: CodeChallengeMethodS256,
CodeChallengeMethodS512.uid: CodeChallengeMethodS512,
}
// ParseCodeChallengeMethod parse string identifier of code challenge method
// into struct enum.
func ParseCodeChallengeMethod(slug string) (CodeChallengeMethod, error) {
if method, ok := slugsMethods[strings.ToUpper(slug)]; ok {
func ParseCodeChallengeMethod(uid string) (CodeChallengeMethod, error) {
if method, ok := slugsMethods[strings.ToUpper(uid)]; ok {
return method, nil
}
return CodeChallengeMethodUndefined, fmt.Errorf("%w: %s", ErrCodeChallengeMethodUnknown, slug)
return CodeChallengeMethodUndefined, fmt.Errorf("%w: %s", ErrCodeChallengeMethodUnknown, uid)
}
// UnmarshalForm implements custom unmarshler for form values.
func (ccm *CodeChallengeMethod) UnmarshalForm(v []byte) error {
method, err := ParseCodeChallengeMethod(string(v))
if err != nil {
return fmt.Errorf("code_challenge_method: %w", err)
return fmt.Errorf("UnmarshalForm: %w", err)
}
*ccm = method
@ -86,15 +87,16 @@ func (ccm *CodeChallengeMethod) UnmarshalForm(v []byte) error {
return nil
}
// UnmarshalJSON implements custom unmarshler for JSON.
func (ccm *CodeChallengeMethod) UnmarshalJSON(v []byte) error {
src, err := strconv.Unquote(string(v))
if err != nil {
return err
return fmt.Errorf("UnmarshalJSON: %w", err)
}
method, err := ParseCodeChallengeMethod(src)
if err != nil {
return fmt.Errorf("code_challenge_method: %w", err)
return fmt.Errorf("UnmarshalJSON: %w", err)
}
*ccm = method
@ -104,15 +106,17 @@ func (ccm *CodeChallengeMethod) UnmarshalJSON(v []byte) error {
// String returns string representation of code challenge method.
func (ccm CodeChallengeMethod) String() string {
return ccm.slug
return ccm.uid
}
// Validate checks for a match to the verifier with the hashed version of the
// challenge via the chosen method.
func (ccm CodeChallengeMethod) Validate(codeChallenge, verifier string) bool {
if ccm.slug == CodeChallengeMethodUndefined.slug {
if ccm.uid == CodeChallengeMethodUndefined.uid {
return false
}
if ccm.slug == CodeChallengeMethodPLAIN.slug {
if ccm.uid == CodeChallengeMethodPLAIN.uid {
return codeChallenge == verifier
}

View File

@ -62,9 +62,20 @@ type (
Expiry time.Duration `yaml:"expiry"` // 1m
Length int `yaml:"length"` // 24
}
ConfigRelMeAuth struct {
Enabled bool `yaml:"enabled"` // true
Providers []ConfigRelMeAuthProvider `yaml:"providers"`
}
ConfigRelMeAuthProvider struct {
Type string `yaml:"type"`
ID string `yaml:"id"`
Secret string `yaml:"secret"`
}
)
// TestConfig returns a valid *viper.Viper with the generated test data filled in.
// TestConfig returns a valid config for tests.
func TestConfig(tb testing.TB) *Config {
tb.Helper()
@ -112,7 +123,7 @@ func (cs ConfigServer) GetAddress() string {
return net.JoinHostPort(cs.Host, cs.Port)
}
// GetRootURL returns generated from template RootURL.
// GetRootURL returns generated root URL from template RootURL.
func (cs ConfigServer) GetRootURL() string {
return fasttemplate.ExecuteString(cs.RootURL, `{{`, `}}`, map[string]interface{}{
"domain": cs.Domain,

View File

@ -3,23 +3,17 @@ package domain
import (
"strings"
"testing"
"golang.org/x/xerrors"
)
// Email represent email identifier.
type Email struct {
user string
host string
}
var ErrEmailInvalid error = Error{
Code: ErrorCodeInvalidRequest,
Description: "cannot parse email",
URI: "",
State: "",
frame: xerrors.Caller(1),
}
var ErrEmailInvalid error = NewError(ErrorCodeInvalidRequest, "cannot parse email")
// ParseEmail parse strings to email identifier.
func ParseEmail(src string) (*Email, error) {
parts := strings.Split(strings.TrimPrefix(src, "mailto:"), "@")
if len(parts) != 2 { //nolint: gomnd
@ -32,6 +26,7 @@ func ParseEmail(src string) (*Email, error) {
}, nil
}
// TestEmail returns valid random generated email identifier.
func TestEmail(tb testing.TB) *Email {
tb.Helper()
@ -41,6 +36,7 @@ func TestEmail(tb testing.TB) *Email {
}
}
// String returns string representation of email identifier.
func (e Email) String() string {
return e.user + "@" + e.host
}

View File

@ -140,6 +140,10 @@ func (e Error) FormatError(p xerrors.Printer) error {
p.Print(": ", e.Description)
}
if !p.Detail() {
return nil
}
e.frame.Format(p)
return nil

View File

@ -10,31 +10,33 @@ import (
// NOTE(toby3d): Encapsulate enums in structs for extra compile-time safety:
// https://threedots.tech/post/safer-enums-in-go/#struct-based-enums
type GrantType struct {
slug string
uid string
}
//nolint: gochecknoglobals // NOTE(toby3d): structs cannot be constants
var (
GrantTypeUndefined = GrantType{slug: ""}
GrantTypeAuthorizationCode = GrantType{slug: "authorization_code"}
GrantTypeUndefined = GrantType{uid: ""}
GrantTypeAuthorizationCode = GrantType{uid: "authorization_code"}
// TicketAuth extension.
GrantTypeTicket = GrantType{slug: "ticket"}
GrantTypeTicket = GrantType{uid: "ticket"}
)
var ErrGrantTypeUnknown = errors.New("unknown grant type")
var ErrGrantTypeUnknown error = errors.New("unknown grant type")
func ParseGrantType(slug string) (GrantType, error) {
switch strings.ToLower(slug) {
case GrantTypeAuthorizationCode.slug:
// ParseGrantType parse grant_type value as GrantType struct enum.
func ParseGrantType(uid string) (GrantType, error) {
switch strings.ToLower(uid) {
case GrantTypeAuthorizationCode.uid:
return GrantTypeAuthorizationCode, nil
case GrantTypeTicket.slug:
case GrantTypeTicket.uid:
return GrantTypeTicket, nil
}
return GrantTypeUndefined, fmt.Errorf("%w: %s", ErrGrantTypeUnknown, slug)
return GrantTypeUndefined, fmt.Errorf("%w: %s", ErrGrantTypeUnknown, uid)
}
// UnmarshalForm implements custom unmarshler for form values.
func (gt *GrantType) UnmarshalForm(src []byte) error {
responseType, err := ParseGrantType(string(src))
if err != nil {
@ -46,6 +48,7 @@ func (gt *GrantType) UnmarshalForm(src []byte) error {
return nil
}
// UnmarshalJSON implements custom unmarshler for JSON.
func (gt *GrantType) UnmarshalJSON(v []byte) error {
src, err := strconv.Unquote(string(v))
if err != nil {
@ -62,6 +65,7 @@ func (gt *GrantType) UnmarshalJSON(v []byte) error {
return nil
}
// String returns string representation of grant type.
func (gt GrantType) String() string {
return gt.slug
return gt.uid
}

View File

@ -18,6 +18,7 @@ type Me struct {
me *http.URI
}
// ParseMe parse string as me URL identifier.
//nolint: funlen
func ParseMe(raw string) (*Me, error) {
me := http.AcquireURI()
@ -26,6 +27,7 @@ func ParseMe(raw string) (*Me, error) {
Code: ErrorCodeInvalidRequest,
Description: err.Error(),
URI: "https://indieauth.net/source/#user-profile-url",
State: "",
frame: xerrors.Caller(1),
}
}
@ -36,6 +38,7 @@ func ParseMe(raw string) (*Me, error) {
Code: ErrorCodeInvalidRequest,
Description: "profile URL MUST have either an https or http scheme",
URI: "https://indieauth.net/source/#user-profile-url",
State: "",
frame: xerrors.Caller(1),
}
}
@ -47,6 +50,7 @@ func ParseMe(raw string) (*Me, error) {
Description: "profile URL MUST contain a path component (/ is a valid path), MUST NOT " +
"contain single-dot or double-dot path segments",
URI: "https://indieauth.net/source/#user-profile-url",
State: "",
frame: xerrors.Caller(1),
}
}
@ -56,6 +60,7 @@ func ParseMe(raw string) (*Me, error) {
Code: ErrorCodeInvalidRequest,
Description: "profile URL MUST NOT contain a fragment component",
URI: "https://indieauth.net/source/#user-profile-url",
State: "",
frame: xerrors.Caller(1),
}
}
@ -65,6 +70,7 @@ func ParseMe(raw string) (*Me, error) {
Code: ErrorCodeInvalidRequest,
Description: "profile URL MUST NOT contain a username or password component",
URI: "https://indieauth.net/source/#user-profile-url",
State: "",
frame: xerrors.Caller(1),
}
}
@ -75,6 +81,7 @@ func ParseMe(raw string) (*Me, error) {
Code: ErrorCodeInvalidRequest,
Description: "profile host name MUST be a domain name",
URI: "https://indieauth.net/source/#user-profile-url",
State: "",
frame: xerrors.Caller(1),
}
}
@ -84,6 +91,7 @@ func ParseMe(raw string) (*Me, error) {
Code: ErrorCodeInvalidRequest,
Description: "profile MUST NOT contain a port",
URI: "https://indieauth.net/source/#user-profile-url",
State: "",
frame: xerrors.Caller(1),
}
}
@ -93,6 +101,7 @@ func ParseMe(raw string) (*Me, error) {
Code: ErrorCodeInvalidRequest,
Description: "profile MUST NOT be ipv4 or ipv6 addresses",
URI: "https://indieauth.net/source/#user-profile-url",
State: "",
frame: xerrors.Caller(1),
}
}
@ -100,7 +109,7 @@ func ParseMe(raw string) (*Me, error) {
return &Me{me: me}, nil
}
// TestMe returns a valid random generated Me for tests.
// TestMe returns valid random generated me for tests.
func TestMe(tb testing.TB, src string) *Me {
tb.Helper()
@ -110,7 +119,7 @@ func TestMe(tb testing.TB, src string) *Me {
return me
}
// UnmarshalForm parses the value of the form key into the Me domain.
// UnmarshalForm implements custom unmarshler for form values.
func (m *Me) UnmarshalForm(v []byte) error {
me, err := ParseMe(string(v))
if err != nil {
@ -122,15 +131,16 @@ func (m *Me) UnmarshalForm(v []byte) error {
return nil
}
// UnmarshalJSON implements custom unmarshler for JSON.
func (m *Me) UnmarshalJSON(v []byte) error {
src, err := strconv.Unquote(string(v))
if err != nil {
return err
return fmt.Errorf("UnmarshalJSON: %w", err)
}
me, err := ParseMe(src)
if err != nil {
return fmt.Errorf("UnmarshalForm: %w", err)
return fmt.Errorf("UnmarshalJSON: %w", err)
}
*m = *me
@ -138,11 +148,12 @@ func (m *Me) UnmarshalJSON(v []byte) error {
return nil
}
// MarshalJSON implements custom marshler for JSON.
func (m Me) MarshalJSON() ([]byte, error) {
return []byte(strconv.Quote(m.String())), nil
}
// URI returns copy of parsed Me in *fasthttp.URI representation.
// URI returns copy of parsed me in *fasthttp.URI representation.
// This copy MUST be released via fasthttp.ReleaseURI.
func (m Me) URI() *http.URI {
if m.me == nil {
@ -155,7 +166,7 @@ func (m Me) URI() *http.URI {
return u
}
// URL returns copy of parsed Me in *url.URL representation.
// URL returns copy of parsed me in *url.URL representation.
func (m Me) URL() *url.URL {
if m.me == nil {
return nil
@ -171,7 +182,7 @@ func (m Me) URL() *url.URL {
}
}
// String returns string representation of Me.
// String returns string representation of me.
func (m Me) String() string {
if m.me == nil {
return ""

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:
// https://threedots.tech/post/safer-enums-in-go/#struct-based-enums
type ResponseType struct {
slug string
uid string
}
//nolint: gochecknoglobals // NOTE(toby3d): structs cannot be constants
var (
ResponseTypeUndefined ResponseType = ResponseType{slug: ""}
ResponseTypeUndefined ResponseType = ResponseType{uid: ""}
// Deprecated(toby3d): Only accept response_type=code requests, and for
// backwards-compatible support, treat response_type=id requests as
// response_type=code requests:
// https://aaronparecki.com/2020/12/03/1/indieauth-2020#response-type
ResponseTypeID ResponseType = ResponseType{slug: "id"}
ResponseTypeID ResponseType = ResponseType{uid: "id"}
// Indicates to the authorization server that an authorization code
// should be returned as the response:
// https://indieauth.net/source/#authorization-request-li-1
ResponseTypeCode ResponseType = ResponseType{slug: "code"}
ResponseTypeCode ResponseType = ResponseType{uid: "code"}
)
var ErrResponseTypeUnknown = errors.New("unknown grant type")
var ErrResponseTypeUnknown error = errors.New("unknown grant type")
func ParseResponseType(slug string) (ResponseType, error) {
switch strings.ToLower(slug) {
case ResponseTypeCode.slug:
// ParseResponseType parse string as response type struct enum.
func ParseResponseType(uid string) (ResponseType, error) {
switch strings.ToLower(uid) {
case ResponseTypeCode.uid:
return ResponseTypeCode, nil
case ResponseTypeID.slug:
case ResponseTypeID.uid:
return ResponseTypeID, nil
}
return ResponseTypeUndefined, fmt.Errorf("%w: %s", ErrResponseTypeUnknown, slug)
return ResponseTypeUndefined, fmt.Errorf("%w: %s", ErrResponseTypeUnknown, uid)
}
// UnmarshalForm implements custom unmarshler for form values.
func (rt *ResponseType) UnmarshalForm(src []byte) error {
responseType, err := ParseResponseType(string(src))
if err != nil {
return fmt.Errorf("response_type: %w", err)
return fmt.Errorf("UnmarshalForm: %w", err)
}
*rt = responseType
@ -53,15 +55,16 @@ func (rt *ResponseType) UnmarshalForm(src []byte) error {
return nil
}
// UnmarshalJSON implements custom unmarshler for JSON.
func (rt *ResponseType) UnmarshalJSON(v []byte) error {
src, err := strconv.Unquote(string(v))
uid, err := strconv.Unquote(string(v))
if err != nil {
return err
return fmt.Errorf("UnmarshalJSON: %w", err)
}
responseType, err := ParseResponseType(string(src))
responseType, err := ParseResponseType(string(uid))
if err != nil {
return fmt.Errorf("response_type: %w", err)
return fmt.Errorf("UnmarshalJSON: %w", err)
}
*rt = responseType
@ -69,6 +72,7 @@ func (rt *ResponseType) UnmarshalJSON(v []byte) error {
return nil
}
// String returns string representation of response type.
func (rt ResponseType) String() string {
return rt.slug
return rt.uid
}

View File

@ -1,7 +1,6 @@
package domain
import (
"errors"
"fmt"
"sort"
"strconv"
@ -9,40 +8,43 @@ import (
)
type (
// NOTE(toby3d): https://threedots.tech/post/safer-enums-in-go/#struct-based-enums
// Scope represent single token scope supported by IndieAuth.
//
// NOTE(toby3d): Encapsulate enums in structs for extra compile-time safety:
// https://threedots.tech/post/safer-enums-in-go/#struct-based-enums
Scope struct {
slug string
uid string
}
// Scopes represent set of Scope domains.
Scopes []Scope
)
var ErrScopeUnknown = errors.New("unknown scope")
var ErrScopeUnknown error = NewError(ErrorCodeInvalidRequest, "unknown scope")
//nolint: gochecknoglobals // NOTE(toby3d): structs cannot be constants
var (
ScopeUndefined = Scope{slug: ""}
ScopeUndefined = Scope{uid: ""}
// https://indieweb.org/scope#Micropub_Scopes
ScopeCreate = Scope{slug: "create"}
ScopeDelete = Scope{slug: "delete"}
ScopeDraft = Scope{slug: "draft"}
ScopeMedia = Scope{slug: "media"}
ScopeUpdate = Scope{slug: "update"}
ScopeCreate = Scope{uid: "create"}
ScopeDelete = Scope{uid: "delete"}
ScopeDraft = Scope{uid: "draft"}
ScopeMedia = Scope{uid: "media"}
ScopeUpdate = Scope{uid: "update"}
// https://indieweb.org/scope#Microsub_Scopes
ScopeBlock = Scope{slug: "block"}
ScopeChannels = Scope{slug: "channels"}
ScopeFollow = Scope{slug: "follow"}
ScopeMute = Scope{slug: "mute"}
ScopeRead = Scope{slug: "read"}
ScopeBlock = Scope{uid: "block"}
ScopeChannels = Scope{uid: "channels"}
ScopeFollow = Scope{uid: "follow"}
ScopeMute = Scope{uid: "mute"}
ScopeRead = Scope{uid: "read"}
// This scope requests access to the user's default profile information
// which include the following properties: name, `photo, url.
//
// NOTE(toby3d): https://indieauth.net/source/#profile-information
ScopeProfile = Scope{slug: "profile"}
ScopeProfile = Scope{uid: "profile"}
// This scope requests access to the user's email address in the
// following property: email.
@ -52,45 +54,49 @@ var (
// and must be requested along with the profile scope if desired.
//
// NOTE(toby3d): https://indieauth.net/source/#profile-information
ScopeEmail = Scope{slug: "email"}
ScopeEmail = Scope{uid: "email"}
)
//nolint: gochecknoglobals // NOTE(toby3d): maps cannot be constants
var slugsScopes = map[string]Scope{
ScopeBlock.slug: ScopeBlock,
ScopeChannels.slug: ScopeChannels,
ScopeCreate.slug: ScopeCreate,
ScopeDelete.slug: ScopeDelete,
ScopeDraft.slug: ScopeDraft,
ScopeEmail.slug: ScopeEmail,
ScopeFollow.slug: ScopeFollow,
ScopeMedia.slug: ScopeMedia,
ScopeMute.slug: ScopeMute,
ScopeProfile.slug: ScopeProfile,
ScopeRead.slug: ScopeRead,
ScopeUpdate.slug: ScopeUpdate,
var uidsScopes = map[string]Scope{
ScopeBlock.uid: ScopeBlock,
ScopeChannels.uid: ScopeChannels,
ScopeCreate.uid: ScopeCreate,
ScopeDelete.uid: ScopeDelete,
ScopeDraft.uid: ScopeDraft,
ScopeEmail.uid: ScopeEmail,
ScopeFollow.uid: ScopeFollow,
ScopeMedia.uid: ScopeMedia,
ScopeMute.uid: ScopeMute,
ScopeProfile.uid: ScopeProfile,
ScopeRead.uid: ScopeRead,
ScopeUpdate.uid: ScopeUpdate,
}
// ParseScope parses scope slug into Scope domain.
func ParseScope(slug string) (Scope, error) {
if scope, ok := slugsScopes[strings.ToLower(slug)]; ok {
func ParseScope(uid string) (Scope, error) {
if scope, ok := uidsScopes[strings.ToLower(uid)]; ok {
return scope, nil
}
return ScopeUndefined, fmt.Errorf("%w: %s", ErrScopeUnknown, slug)
return ScopeUndefined, fmt.Errorf("%w: %s", ErrScopeUnknown, uid)
}
// UnmarshalForm parses the value of the form key into the Scope domain.
// UnmarshalForm implements custom unmarshler for form values.
func (s *Scopes) UnmarshalForm(v []byte) error {
scopes := make(Scopes, 0)
for _, rawScope := range strings.Fields(string(v)) {
scope, err := ParseScope(rawScope)
if err != nil {
return fmt.Errorf("scopes: %w", err)
return fmt.Errorf("UnmarshalForm: %w", err)
}
*s = append(scopes, scope)
if scopes.Has(scope) {
continue
}
scopes = append(scopes, scope)
}
*s = scopes
@ -98,18 +104,23 @@ func (s *Scopes) UnmarshalForm(v []byte) error {
return nil
}
// UnmarshalJSON implements custom unmarshler for JSON.
func (s *Scopes) UnmarshalJSON(v []byte) error {
src, err := strconv.Unquote(string(v))
if err != nil {
return err
return fmt.Errorf("UnmarshalJSON: %w", err)
}
result := make([]Scope, 0)
result := make(Scopes, 0)
for _, scope := range strings.Fields(src) {
s, err := ParseScope(scope)
if err != nil {
return fmt.Errorf("scope: %w", err)
return fmt.Errorf("UnmarshalJSON: %w", err)
}
if result.Has(s) {
continue
}
result = append(result, s)
@ -120,6 +131,7 @@ func (s *Scopes) UnmarshalJSON(v []byte) error {
return nil
}
// UnmarshalJSON implements custom marshler for JSON.
func (s Scopes) MarshalJSON() ([]byte, error) {
scopes := make([]string, len(s))
@ -132,11 +144,12 @@ func (s Scopes) MarshalJSON() ([]byte, error) {
return []byte(strconv.Quote(strings.Join(scopes, " "))), nil
}
// String returns scope slug as string.
// String returns string representation of scope.
func (s Scope) String() string {
return s.slug
return s.uid
}
// String returns string representation of scopes.
func (s Scopes) String() string {
scopes := make([]string, len(s))
@ -146,3 +159,29 @@ func (s Scopes) String() string {
return strings.Join(scopes, " ")
}
// IsEmpty returns true if the set does not contain valid scope.
func (s Scopes) IsEmpty() bool {
for i := range s {
if s[i] == ScopeUndefined {
continue
}
return false
}
return true
}
// Has check what input scope contains in current scopes collection.
func (s Scopes) Has(scope Scope) bool {
for i := range s {
if s[i] != scope {
continue
}
return true
}
return false
}

View File

@ -1,27 +1,41 @@
package domain
import "testing"
import (
"testing"
"github.com/stretchr/testify/require"
"source.toby3d.me/website/indieauth/internal/random"
)
type Session struct {
ClientID *ClientID
Me *Me
RedirectURI *URL
Profile *Profile
CodeChallengeMethod CodeChallengeMethod
Scope Scopes
Code string
CodeChallenge string
}
// TestSession returns valid random generated session for tests.
func TestSession(tb testing.TB) *Session {
tb.Helper()
code, err := random.String(24)
require.NoError(tb, err)
return &Session{
ClientID: TestClientID(tb),
Code: code,
CodeChallenge: "hackme",
CodeChallengeMethod: CodeChallengeMethodPLAIN,
Me: TestMe(tb, "https://user.example.net/"),
RedirectURI: TestURL(tb, "https://example.com/callback"),
CodeChallengeMethod: CodeChallengeMethodPLAIN,
Scope: Scopes{ScopeProfile, ScopeEmail},
Code: "abcdefg",
CodeChallenge: "hackme",
Scope: Scopes{
ScopeEmail,
ScopeProfile,
},
}
}

View File

@ -15,6 +15,7 @@ type Ticket struct {
Subject *Me
}
// TestTicket returns valid random generated ticket for tests.
func TestTicket(tb testing.TB) *Ticket {
tb.Helper()

View File

@ -16,28 +16,31 @@ import (
type (
// Token describes the data of the token used by the clients.
Token struct {
AccessToken string
Scope Scopes
ClientID *ClientID
Me *Me
Scope Scopes
AccessToken string
}
// NewTokenOptions contains options for NewToken function.
NewTokenOptions struct {
Algorithm string
Expiration time.Duration
Issuer *ClientID
NonceLength int
Scope Scopes
Secret []byte
Issuer *ClientID
Subject *Me
Secret []byte
Algorithm string
NonceLength int
}
)
//nolint: gochecknoglobals
var DefaultNewTokenOptions = NewTokenOptions{
Algorithm: "HS256",
NonceLength: 32,
}
// NewToken create a new token by provided options.
func NewToken(opts NewTokenOptions) (*Token, error) {
if opts.NonceLength == 0 {
opts.NonceLength = DefaultNewTokenOptions.NonceLength
@ -55,13 +58,16 @@ func NewToken(opts NewTokenOptions) (*Token, error) {
}
t := jwt.New()
t.Set(jwt.IssuerKey, opts.Issuer.String())
t.Set(jwt.SubjectKey, opts.Subject.String())
t.Set(jwt.NotBeforeKey, now)
t.Set(jwt.IssuedAtKey, now)
t.Set("scope", opts.Scope)
t.Set("nonce", nonce)
if opts.Issuer != nil {
t.Set(jwt.IssuerKey, opts.Issuer.String())
}
if opts.Expiration != 0 {
t.Set(jwt.ExpirationKey, now.Add(opts.Expiration))
}
@ -79,7 +85,7 @@ func NewToken(opts NewTokenOptions) (*Token, error) {
}, err
}
// TestToken returns a valid Token with the generated test data filled in.
// TestToken returns valid random generated token for tests.
func TestToken(tb testing.TB) *Token {
tb.Helper()
@ -129,6 +135,7 @@ func (t Token) SetAuthHeader(r *http.Request) {
r.Header.Set(http.HeaderAuthorization, t.String())
}
// String returns string representation of token.
func (t Token) String() string {
if t.AccessToken == "" {
return ""

View File

@ -1,6 +1,7 @@
package domain
import (
"fmt"
"net/url"
"strconv"
"testing"
@ -8,19 +9,22 @@ import (
http "github.com/valyala/fasthttp"
)
// URL describe any valid HTTP URL.
type URL struct {
*http.URI
}
// ParseURL parse strings as URL.
func ParseURL(src string) (*URL, error) {
u := http.AcquireURI()
if err := u.Parse(nil, []byte(src)); err != nil {
return nil, err
return nil, fmt.Errorf("cannot parse URL: %w", err)
}
return &URL{URI: u}, nil
}
// TestURL returns URL of provided input for tests.
func TestURL(tb testing.TB, src string) *URL {
tb.Helper()
@ -32,10 +36,11 @@ func TestURL(tb testing.TB, src string) *URL {
}
}
// UnmarshalForm implements custom unmarshler for form values.
func (u *URL) UnmarshalForm(v []byte) error {
url, err := ParseURL(string(v))
if err != nil {
return err
return fmt.Errorf("UnmarshalForm: %w", err)
}
*u = *url
@ -43,15 +48,16 @@ func (u *URL) UnmarshalForm(v []byte) error {
return nil
}
// UnmarshalJSON implements custom unmarshler for JSON.
func (u *URL) UnmarshalJSON(v []byte) error {
src, err := strconv.Unquote(string(v))
if err != nil {
return err
return fmt.Errorf("UnmarshalJSON: %w", err)
}
url, err := ParseURL(src)
if err != nil {
return err
return fmt.Errorf("UnmarshalJSON: %w", err)
}
*u = *url
@ -59,6 +65,7 @@ func (u *URL) UnmarshalJSON(v []byte) error {
return nil
}
// URL returns url.URL representation of URL.
func (u URL) URL() *url.URL {
if u.URI == nil {
return nil

View File

@ -15,6 +15,7 @@ type User struct {
*Profile
}
// TestUser returns valid random generated user for tests.
func TestUser(tb testing.TB) *User {
tb.Helper()