diff --git a/internal/auth/delivery/http/auth_http.go b/internal/auth/delivery/http/auth_http.go
index 88a0c98..6097bec 100644
--- a/internal/auth/delivery/http/auth_http.go
+++ b/internal/auth/delivery/http/auth_http.go
@@ -268,18 +268,8 @@ func (h *Handler) handleExchange(w http.ResponseWriter, r *http.Request) {
return
}
- var userInfo *AuthProfileResponse
- if profile != nil {
- userInfo = &AuthProfileResponse{
- Email: profile.GetEmail(),
- Photo: &domain.URL{URL: profile.GetPhoto()},
- URL: &domain.URL{URL: profile.GetURL()},
- Name: profile.GetName(),
- }
- }
-
_ = encoder.Encode(&AuthExchangeResponse{
- Me: *me,
- Profile: userInfo,
+ Me: me.String(),
+ Profile: NewAuthProfileResponse(profile),
})
}
diff --git a/internal/auth/delivery/http/auth_http_schema.go b/internal/auth/delivery/http/auth_http_schema.go
index c8c59a9..e9fa703 100644
--- a/internal/auth/delivery/http/auth_http_schema.go
+++ b/internal/auth/delivery/http/auth_http_schema.go
@@ -79,15 +79,15 @@ type (
}
AuthExchangeResponse struct {
- Me domain.Me `json:"me"`
Profile *AuthProfileResponse `json:"profile,omitempty"`
+ Me string `json:"me"`
}
AuthProfileResponse struct {
- Email *domain.Email `json:"email,omitempty"`
- Photo *domain.URL `json:"photo,omitempty"`
- URL *domain.URL `json:"url,omitempty"`
- Name string `json:"name,omitempty"`
+ Email string `json:"email,omitempty"`
+ Photo string `json:"photo,omitempty"`
+ URL string `json:"url,omitempty"`
+ Name string `json:"name,omitempty"`
}
)
@@ -192,3 +192,27 @@ func (r *AuthExchangeRequest) bind(req *http.Request) error {
return nil
}
+
+func NewAuthProfileResponse(in *domain.Profile) *AuthProfileResponse {
+ out := new(AuthProfileResponse)
+
+ if in == nil {
+ return out
+ }
+
+ out.Name = in.Name
+
+ if in.URL != nil {
+ out.URL = in.URL.String()
+ }
+
+ if in.Email != nil {
+ out.Email = in.Email.String()
+ }
+
+ if in.Photo != nil {
+ out.Photo = in.Photo.String()
+ }
+
+ return out
+}
diff --git a/internal/auth/delivery/http/auth_http_test.go b/internal/auth/delivery/http/auth_http_test.go
index 4012742..5503f60 100644
--- a/internal/auth/delivery/http/auth_http_test.go
+++ b/internal/auth/delivery/http/auth_http_test.go
@@ -31,13 +31,14 @@ type Dependencies struct {
authService auth.UseCase
clients client.Repository
clientService client.UseCase
- config *domain.Config
matcher language.Matcher
profiles profile.Repository
sessions session.Repository
users user.Repository
+ config *domain.Config
}
+//nolint:funlen
func TestAuthorize(t *testing.T) {
t.Parallel()
@@ -98,7 +99,7 @@ func TestAuthorize(t *testing.T) {
t.Errorf("%s %s = %d, want %d", req.Method, u.String(), resp.StatusCode, http.StatusOK)
}
- expResult := `Authorize ` + client.GetName()
+ expResult := `Authorize ` + client.Name
if result := string(body); !strings.Contains(result, expResult) {
t.Errorf("%s %s = %s, want %s", req.Method, u.String(), result, expResult)
}
@@ -113,7 +114,7 @@ func NewDependencies(tb testing.TB) Dependencies {
users := userrepo.NewMemoryUserRepository()
sessions := sessionrepo.NewMemorySessionRepository(*config)
profiles := profilerepo.NewMemoryProfileRepository()
- authService := ucase.NewAuthUseCase(sessions, profiles, config)
+ authService := ucase.NewAuthUseCase(sessions, profiles, *config)
clientService := clientucase.NewClientUseCase(clients)
return Dependencies{
diff --git a/internal/auth/usecase/auth_ucase.go b/internal/auth/usecase/auth_ucase.go
index 0dd4b83..3eed5f9 100644
--- a/internal/auth/usecase/auth_ucase.go
+++ b/internal/auth/usecase/auth_ucase.go
@@ -12,13 +12,13 @@ import (
)
type authUseCase struct {
- config *domain.Config
sessions session.Repository
profiles profile.Repository
+ config domain.Config
}
// NewAuthUseCase creates a new authentication use case.
-func NewAuthUseCase(sessions session.Repository, profiles profile.Repository, config *domain.Config) auth.UseCase {
+func NewAuthUseCase(sessions session.Repository, profiles profile.Repository, config domain.Config) auth.UseCase {
return &authUseCase{
config: config,
sessions: sessions,
diff --git a/internal/client/delivery/http/client_http_test.go b/internal/client/delivery/http/client_http_test.go
index faaa47a..f03ecc2 100644
--- a/internal/client/delivery/http/client_http_test.go
+++ b/internal/client/delivery/http/client_http_test.go
@@ -58,7 +58,7 @@ func NewDependencies(tb testing.TB) Dependencies {
tokens := tokenrepo.NewMemoryTokenRepository()
profiles := profilerepo.NewMemoryProfileRepository()
tokenService := tokenucase.NewTokenUseCase(tokenucase.Config{
- Config: config,
+ Config: *config,
Profiles: profiles,
Sessions: sessions,
Tokens: tokens,
diff --git a/internal/client/repository/http/http_client.go b/internal/client/repository/http/http_client.go
index 58c5d09..fae0762 100644
--- a/internal/client/repository/http/http_client.go
+++ b/internal/client/repository/http/http_client.go
@@ -8,27 +8,40 @@ import (
"net/http"
"net/url"
+ "github.com/tomnomnom/linkheader"
"golang.org/x/exp/slices"
+ "willnorris.com/go/microformats"
"source.toby3d.me/toby3d/auth/internal/client"
"source.toby3d.me/toby3d/auth/internal/common"
"source.toby3d.me/toby3d/auth/internal/domain"
- "source.toby3d.me/toby3d/auth/internal/httputil"
)
-type httpClientRepository struct {
- client *http.Client
-}
+type (
+ //nolint:tagliatelle,lll
+ Response struct {
+ TicketEndpoint domain.URL `json:"ticket_endpoint"`
+ AuthorizationEndpoint domain.URL `json:"authorization_endpoint"`
+ IntrospectionEndpoint domain.URL `json:"introspection_endpoint"`
+ RevocationEndpoint domain.URL `json:"revocation_endpoint,omitempty"`
+ ServiceDocumentation domain.URL `json:"service_documentation,omitempty"`
+ TokenEndpoint domain.URL `json:"token_endpoint"`
+ UserinfoEndpoint domain.URL `json:"userinfo_endpoint,omitempty"`
+ Microsub domain.URL `json:"microsub"`
+ Issuer domain.URL `json:"issuer"`
+ Micropub domain.URL `json:"micropub"`
+ GrantTypesSupported []domain.GrantType `json:"grant_types_supported,omitempty"`
+ IntrospectionEndpointAuthMethodsSupported []string `json:"introspection_endpoint_auth_methods_supported,omitempty"`
+ RevocationEndpointAuthMethodsSupported []string `json:"revocation_endpoint_auth_methods_supported,omitempty"`
+ ScopesSupported []domain.Scope `json:"scopes_supported,omitempty"`
+ ResponseTypesSupported []domain.ResponseType `json:"response_types_supported,omitempty"`
+ CodeChallengeMethodsSupported []domain.CodeChallengeMethod `json:"code_challenge_methods_supported"`
+ AuthorizationResponseIssParameterSupported bool `json:"authorization_response_iss_parameter_supported,omitempty"`
+ }
-const (
- DefaultMaxRedirectsCount int = 10
-
- hApp string = "h-app"
- hXApp string = "h-x-app"
- propertyLogo string = "logo"
- propertyName string = "name"
- propertyURL string = "url"
- relRedirectURI string = "redirect_uri"
+ httpClientRepository struct {
+ client *http.Client
+ }
)
func NewHTTPClientRepository(c *http.Client) client.Repository {
@@ -46,9 +59,9 @@ func (repo httpClientRepository) Get(ctx context.Context, cid domain.ClientID) (
out := &domain.Client{
ID: cid,
RedirectURI: make([]*url.URL, 0),
- Logo: make([]*url.URL, 0),
- URL: make([]*url.URL, 0),
- Name: make([]string, 0),
+ Logo: nil,
+ URL: nil,
+ Name: "",
}
if cid.IsLocalhost() {
@@ -64,71 +77,82 @@ func (repo httpClientRepository) Get(ctx context.Context, cid domain.ClientID) (
return nil, fmt.Errorf("%w: status on client page is not 200", client.ErrNotExist)
}
- extract(resp.Body, resp.Request.URL, out, resp.Header.Get(common.HeaderLink))
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, fmt.Errorf("cannot read response body: %w", err)
+ }
+
+ // NOTE(toby3d): fetch redirect uri's and application profile from HTML nodes
+ mf2 := microformats.Parse(bytes.NewReader(body), resp.Request.URL)
+
+ for i := range mf2.Items {
+ if !slices.Contains(mf2.Items[i].Type, common.HApp) &&
+ !slices.Contains(mf2.Items[i].Type, common.HXApp) {
+ continue
+ }
+
+ parseProfile(mf2.Items[i].Properties, out)
+ }
+
+ for _, val := range mf2.Rels[common.RelRedirectURI] {
+ var u *url.URL
+ if u, err = url.Parse(val); err == nil {
+ out.RedirectURI = append(out.RedirectURI, u)
+ }
+ }
+
+ // NOTE(toby3d): fetch redirect uri's from Link header
+ for _, link := range linkheader.Parse(resp.Header.Get(common.HeaderLink)) {
+ if link.Rel != common.RelRedirectURI {
+ continue
+ }
+
+ var u *url.URL
+ if u, err = url.Parse(link.URL); err == nil {
+ out.RedirectURI = append(out.RedirectURI, u)
+ }
+ }
return out, nil
}
-//nolint:gocognit,cyclop
-func extract(r io.Reader, u *url.URL, dst *domain.Client, header string) {
- body, _ := io.ReadAll(r)
-
- for _, endpoint := range httputil.ExtractEndpoints(bytes.NewReader(body), u, header, relRedirectURI) {
- if !containsUrl(dst.RedirectURI, endpoint) {
- dst.RedirectURI = append(dst.RedirectURI, endpoint)
- }
- }
-
- for _, itemType := range []string{hApp, hXApp} {
- for _, name := range httputil.ExtractProperty(bytes.NewReader(body), u, itemType, propertyName) {
- if n, ok := name.(string); ok && !slices.Contains(dst.Name, n) {
- dst.Name = append(dst.Name, n)
- }
- }
-
- for _, logo := range httputil.ExtractProperty(bytes.NewReader(body), u, itemType, propertyLogo) {
- var (
- logoURL *url.URL
- err error
- )
-
- switch l := logo.(type) {
- case string:
- logoURL, err = url.Parse(l)
- case map[string]string:
- if value, ok := l["value"]; ok {
- logoURL, err = url.Parse(value)
- }
- }
-
- if err != nil || containsUrl(dst.Logo, logoURL) {
- continue
- }
-
- dst.Logo = append(dst.Logo, logoURL)
- }
-
- for _, property := range httputil.ExtractProperty(bytes.NewReader(body), u, itemType, propertyURL) {
- prop, ok := property.(string)
- if !ok {
- continue
- }
-
- if u, err := url.Parse(prop); err == nil && !containsUrl(dst.URL, u) {
- dst.URL = append(dst.URL, u)
- }
- }
- }
-}
-
-func containsUrl(src []*url.URL, find *url.URL) bool {
- for i := range src {
- if src[i].String() != find.String() {
+func parseProfile(src map[string][]any, dst *domain.Client) {
+ for _, val := range src[common.PropertyName] {
+ v, ok := val.(string)
+ if !ok {
continue
}
- return true
+ dst.Name = v
+
+ break
}
- return false
+ for _, val := range src[common.PropertyURL] {
+ v, ok := val.(string)
+ if !ok {
+ continue
+ }
+
+ var err error
+ if dst.URL, err = url.Parse(v); err != nil {
+ continue
+ }
+
+ break
+ }
+
+ for _, val := range src[common.PropertyLogo] {
+ v, ok := val.(string)
+ if !ok {
+ continue
+ }
+
+ var err error
+ if dst.Logo, err = url.Parse(v); err != nil {
+ continue
+ }
+
+ break
+ }
}
diff --git a/internal/client/repository/http/http_client_test.go b/internal/client/repository/http/http_client_test.go
index d1fb87a..6c36df4 100644
--- a/internal/client/repository/http/http_client_test.go
+++ b/internal/client/repository/http/http_client_test.go
@@ -75,7 +75,7 @@ func testHandler(tb testing.TB, client domain.Client) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set(common.HeaderContentType, common.MIMETextHTMLCharsetUTF8)
- w.Header().Set(common.HeaderLink, `<`+client.RedirectURI[0].String()+`>; rel="redirect_uri"`)
- fmt.Fprintf(w, testBody, client.Name[0], client.URL[0], client.Logo[0], client.RedirectURI[1])
+ w.Header().Set(common.HeaderLink, `<`+client.RedirectURI[1].String()+`>; rel="redirect_uri"`)
+ fmt.Fprintf(w, testBody, client.Name, client.URL, client.Logo, client.RedirectURI[0])
})
}
diff --git a/internal/common/common.go b/internal/common/common.go
index 9a5a60f..09db682 100644
--- a/internal/common/common.go
+++ b/internal/common/common.go
@@ -27,4 +27,29 @@ const (
HeaderXCSRFToken string = "X-CSRF-Token"
)
+const (
+ HApp string = "h-app"
+ HCard string = "h-card"
+ HXApp string = "h-x-app"
+)
+
+const (
+ PropertyEmail string = "email"
+ PropertyLogo string = "logo"
+ PropertyName string = "name"
+ PropertyPhoto string = "photo"
+ PropertyURL string = "url"
+)
+
+const (
+ RelAuthn string = "authn"
+ RelAuthorizationEndpoint string = "authorization_endpoint"
+ RelIndieAuthMetadata string = "indieauth-metadata"
+ RelMicropub string = "micropub"
+ RelMicrosub string = "microsub"
+ RelRedirectURI string = "redirect_uri"
+ RelTicketEndpoint string = "ticket_endpoint"
+ RelTokenEndpoint string = "token_endpoint"
+)
+
const Und string = "und"
diff --git a/internal/domain/client.go b/internal/domain/client.go
index 44131a3..9bf8821 100644
--- a/internal/domain/client.go
+++ b/internal/domain/client.go
@@ -9,21 +9,21 @@ import (
// Client describes the client requesting data about the user.
type Client struct {
+ Logo *url.URL
+ URL *url.URL
ID ClientID
- Logo []*url.URL
+ Name string
RedirectURI []*url.URL
- URL []*url.URL
- Name []string
}
// NewClient creates a new empty Client with provided ClientID, if any.
func NewClient(cid ClientID) *Client {
return &Client{
ID: cid,
- Logo: make([]*url.URL, 0),
+ Logo: nil,
RedirectURI: make([]*url.URL, 0),
- URL: make([]*url.URL, 0),
- Name: make([]string, 0),
+ URL: nil,
+ Name: "",
}
}
@@ -33,9 +33,9 @@ func TestClient(tb testing.TB) *Client {
return &Client{
ID: *TestClientID(tb),
- Name: []string{"Example App"},
- URL: []*url.URL{{Scheme: "https", Host: "app.example.com", Path: "/"}},
- Logo: []*url.URL{{Scheme: "https", Host: "app.example.com", Path: "/logo.png"}},
+ Name: "Example App",
+ URL: &url.URL{Scheme: "https", Host: "app.example.com", Path: "/"},
+ Logo: &url.URL{Scheme: "https", Host: "app.example.com", Path: "/logo.png"},
RedirectURI: []*url.URL{
{Scheme: "https", Host: "app.example.com", Path: "/redirect"},
{Scheme: "https", Host: "app.example.net", Path: "/redirect"},
@@ -81,30 +81,3 @@ func (c *Client) ValidateRedirectURI(redirectURI *url.URL) bool {
return false
}
-
-// GetName safe returns first name, if any.
-func (c Client) GetName() string {
- if len(c.Name) == 0 {
- return ""
- }
-
- return c.Name[0]
-}
-
-// GetURL safe returns first URL, if any.
-func (c Client) GetURL() *url.URL {
- if len(c.URL) == 0 {
- return nil
- }
-
- return c.URL[0]
-}
-
-// GetLogo safe returns first logo, if any.
-func (c Client) GetLogo() *url.URL {
- if len(c.Logo) == 0 {
- return nil
- }
-
- return c.Logo[0]
-}
diff --git a/internal/domain/client_test.go b/internal/domain/client_test.go
index ae7cef8..0d376ac 100644
--- a/internal/domain/client_test.go
+++ b/internal/domain/client_test.go
@@ -13,8 +13,8 @@ func TestClient_ValidateRedirectURI(t *testing.T) {
client := domain.TestClient(t)
for name, in := range map[string]*url.URL{
- "client_id prefix": client.ID.URL().JoinPath("/callback"),
- "registered redirect_uri": client.RedirectURI[len(client.RedirectURI)-1],
+ "prefix": client.ID.URL().JoinPath("/callback"),
+ "redirect_uri": client.RedirectURI[len(client.RedirectURI)-1],
} {
name, in := name, in
@@ -27,30 +27,3 @@ func TestClient_ValidateRedirectURI(t *testing.T) {
})
}
}
-
-func TestClient_GetName(t *testing.T) {
- t.Parallel()
-
- client := domain.TestClient(t)
- if result := client.GetName(); result != client.Name[0] {
- t.Errorf("GetName() = %v, want %v", result, client.Name[0])
- }
-}
-
-func TestClient_GetURL(t *testing.T) {
- t.Parallel()
-
- client := domain.TestClient(t)
- if result := client.GetURL(); result != client.URL[0] {
- t.Errorf("GetURL() = %v, want %v", result, client.URL[0])
- }
-}
-
-func TestClient_GetLogo(t *testing.T) {
- t.Parallel()
-
- client := domain.TestClient(t)
- if result := client.GetLogo(); result != client.Logo[0] {
- t.Errorf("GetLogo() = %v, want %v", result, client.Logo[0])
- }
-}
diff --git a/internal/domain/profile.go b/internal/domain/profile.go
index 55fdb01..7c2272e 100644
--- a/internal/domain/profile.go
+++ b/internal/domain/profile.go
@@ -7,18 +7,18 @@ import (
// Profile describes the data about the user.
type Profile struct {
- Photo []*url.URL `json:"photo,omitempty"`
- URL []*url.URL `json:"url,omitempty"`
- Email []*Email `json:"email,omitempty"`
- Name []string `json:"name,omitempty"`
+ Photo *url.URL `json:"photo,omitempty"`
+ URL *url.URL `json:"url,omitempty"`
+ Email *Email `json:"email,omitempty"`
+ Name string `json:"name,omitempty"`
}
func NewProfile() *Profile {
return &Profile{
- Photo: make([]*url.URL, 0),
- URL: make([]*url.URL, 0),
- Email: make([]*Email, 0),
- Name: make([]string, 0),
+ Photo: new(url.URL),
+ URL: new(url.URL),
+ Email: new(Email),
+ Name: "",
}
}
@@ -27,61 +27,9 @@ func TestProfile(tb testing.TB) *Profile {
tb.Helper()
return &Profile{
- Email: []*Email{TestEmail(tb)},
- Name: []string{"Example User"},
- Photo: []*url.URL{{Scheme: "https", Host: "user.example.net", Path: "/photo.jpg"}},
- URL: []*url.URL{{Scheme: "https", Host: "user.example.net", Path: "/"}},
+ Email: TestEmail(tb),
+ Name: "Example User",
+ Photo: &url.URL{Scheme: "https", Host: "user.example.net", Path: "/photo.jpg"},
+ URL: &url.URL{Scheme: "https", Host: "user.example.net", Path: "/"},
}
}
-
-func (p Profile) HasName() bool {
- return len(p.Name) > 0
-}
-
-// GetName safe returns first name, if any.
-func (p Profile) GetName() string {
- if len(p.Name) == 0 {
- return ""
- }
-
- return p.Name[0]
-}
-
-func (p Profile) HasURL() bool {
- return len(p.URL) > 0
-}
-
-// GetURL safe returns first URL, if any.
-func (p Profile) GetURL() *url.URL {
- if len(p.URL) == 0 {
- return nil
- }
-
- return p.URL[0]
-}
-
-func (p Profile) HasPhoto() bool {
- return len(p.Photo) > 0
-}
-
-// GetPhoto safe returns first photo, if any.
-func (p Profile) GetPhoto() *url.URL {
- if len(p.Photo) == 0 {
- return nil
- }
-
- return p.Photo[0]
-}
-
-func (p Profile) HasEmail() bool {
- return len(p.Email) > 0
-}
-
-// GetEmail safe returns first email, if any.
-func (p Profile) GetEmail() *Email {
- if len(p.Email) == 0 {
- return nil
- }
-
- return p.Email[0]
-}
diff --git a/internal/domain/ticket.go b/internal/domain/ticket.go
index 06ccc4f..7e1db9a 100644
--- a/internal/domain/ticket.go
+++ b/internal/domain/ticket.go
@@ -10,7 +10,7 @@ type Ticket struct {
Resource *url.URL
// The access token should be used when acting on behalf of this URL.
- Subject *Me
+ Subject Me
// A random string that can be redeemed for an access token.
Ticket string
@@ -22,7 +22,7 @@ func TestTicket(tb testing.TB) *Ticket {
return &Ticket{
Resource: &url.URL{Scheme: "https", Host: "alice.example.com", Path: "/private/"},
- Subject: TestMe(tb, "https://bob.example.com/"),
+ Subject: *TestMe(tb, "https://bob.example.com/"),
Ticket: "32985723984723985792834",
}
}
diff --git a/internal/httputil/httputil.go b/internal/httputil/httputil.go
deleted file mode 100644
index 212c1f1..0000000
--- a/internal/httputil/httputil.go
+++ /dev/null
@@ -1,138 +0,0 @@
-package httputil
-
-import (
- "bytes"
- "fmt"
- "io"
- "net/http"
- "net/url"
- "strings"
-
- "github.com/goccy/go-json"
- "github.com/tomnomnom/linkheader"
- "golang.org/x/exp/slices"
- "willnorris.com/go/microformats"
-
- "source.toby3d.me/toby3d/auth/internal/common"
- "source.toby3d.me/toby3d/auth/internal/domain"
-)
-
-const RelIndieauthMetadata = "indieauth-metadata"
-
-var ErrEndpointNotExist = domain.NewError(
- domain.ErrorCodeServerError,
- "cannot found any endpoints",
- "https://indieauth.net/source/#discovery-0",
-)
-
-func ExtractFromMetadata(client *http.Client, u string) (*domain.Metadata, error) {
- req, err := http.NewRequest(http.MethodGet, u, nil)
- if err != nil {
- return nil, err
- }
-
- resp, err := client.Do(req)
- if err != nil {
- return nil, err
- }
-
- body, err := io.ReadAll(resp.Body)
- if err != nil {
- return nil, err
- }
-
- buf := bytes.NewBuffer(body)
-
- endpoints := ExtractEndpoints(buf, resp.Request.URL, resp.Header.Get(common.HeaderLink), RelIndieauthMetadata)
- if len(endpoints) == 0 {
- return nil, ErrEndpointNotExist
- }
-
- if resp, err = client.Get(endpoints[len(endpoints)-1].String()); err != nil {
- return nil, fmt.Errorf("failed to fetch metadata endpoint configuration: %w", err)
- }
-
- result := new(domain.Metadata)
- if err = json.NewDecoder(resp.Body).Decode(result); err != nil {
- return nil, fmt.Errorf("cannot unmarshal emtadata configuration: %w", err)
- }
-
- return result, nil
-}
-
-func ExtractEndpoints(body io.Reader, u *url.URL, linkHeader, rel string) []*url.URL {
- results := make([]*url.URL, 0)
-
- urls, err := ExtractEndpointsFromHeader(linkHeader, rel)
- if err == nil {
- results = append(results, urls...)
- }
-
- urls, err = ExtractEndpointsFromBody(body, u, rel)
- if err == nil {
- results = append(results, urls...)
- }
-
- return results
-}
-
-func ExtractEndpointsFromHeader(linkHeader, rel string) ([]*url.URL, error) {
- results := make([]*url.URL, 0)
-
- for _, link := range linkheader.Parse(linkHeader) {
- if !strings.EqualFold(link.Rel, rel) {
- continue
- }
-
- u, err := url.Parse(link.URL)
- if err != nil {
- return nil, fmt.Errorf("cannot parse header endpoint: %w", err)
- }
-
- results = append(results, u)
- }
-
- return results, nil
-}
-
-func ExtractEndpointsFromBody(body io.Reader, u *url.URL, rel string) ([]*url.URL, error) {
- endpoints, ok := microformats.Parse(body, u).Rels[rel]
- if !ok || len(endpoints) == 0 {
- return nil, ErrEndpointNotExist
- }
-
- results := make([]*url.URL, 0)
-
- for i := range endpoints {
- u, err := url.Parse(endpoints[i])
- if err != nil {
- return nil, fmt.Errorf("cannot parse body endpoint: %w", err)
- }
-
- results = append(results, u)
- }
-
- return results, nil
-}
-
-func ExtractProperty(body io.Reader, u *url.URL, itemType, key string) []any {
- if data := microformats.Parse(body, u); data != nil {
- return FindProperty(data.Items, itemType, key)
- }
-
- return nil
-}
-
-func FindProperty(src []*microformats.Microformat, itemType, key string) []any {
- for _, item := range src {
- if slices.Contains(item.Type, itemType) {
- return item.Properties[key]
- }
-
- if result := FindProperty(item.Children, itemType, key); result != nil {
- return result
- }
- }
-
- return nil
-}
diff --git a/internal/httputil/httputil_test.go b/internal/httputil/httputil_test.go
deleted file mode 100644
index 2bb8f74..0000000
--- a/internal/httputil/httputil_test.go
+++ /dev/null
@@ -1,66 +0,0 @@
-package httputil_test
-
-import (
- "io"
- "net/http"
- "net/url"
- "strings"
- "testing"
-
- "github.com/google/go-cmp/cmp"
-
- "source.toby3d.me/toby3d/auth/internal/httputil"
-)
-
-const testBody = `
-
-
-
-
-
-
- Sample Name
-
-
-`
-
-func TestExtractEndpointsFromBody(t *testing.T) {
- t.Parallel()
-
- req, err := http.NewRequest(http.MethodGet, "https://example.com/", nil)
- if err != nil {
- t.Fatal(err)
- }
-
- in := &http.Response{Body: io.NopCloser(strings.NewReader(testBody))}
-
- out, err := httputil.ExtractEndpointsFromBody(in.Body, req.URL, "lipsum")
- if err != nil {
- t.Fatal(err)
- }
-
- exp := []*url.URL{
- {Scheme: "https", Host: "example.com", Path: "/"},
- {Scheme: "https", Host: "example.net", Path: "/"},
- }
-
- if !cmp.Equal(out, exp) {
- t.Errorf(`ExtractProperty(resp, "h-card", "name") = %+s, want %+s`, out, exp)
- }
-}
-
-func TestExtractProperty(t *testing.T) {
- t.Parallel()
-
- req, err := http.NewRequest(http.MethodGet, "https://example.com/", nil)
- if err != nil {
- t.Fatal(err)
- }
-
- in := &http.Response{Body: io.NopCloser(strings.NewReader(testBody))}
-
- if out := httputil.ExtractProperty(in.Body, req.URL, "h-app", "name"); out == nil || out[0] != "Sample Name" {
- t.Errorf(`ExtractProperty(%s, %s, %s) = %+s, want %+s`, req.URL, "h-app", "name", out,
- []string{"Sample Name"})
- }
-}
diff --git a/internal/metadata/repository/http/http_metadata.go b/internal/metadata/repository/http/http_metadata.go
index 011a2cc..146cb02 100644
--- a/internal/metadata/repository/http/http_metadata.go
+++ b/internal/metadata/repository/http/http_metadata.go
@@ -42,8 +42,6 @@ type (
}
)
-const relIndieauthMetadata = "indieauth-metadata"
-
func NewHTTPMetadataRepository(client *http.Client) metadata.Repository {
return &httpMetadataRepository{
client: client,
diff --git a/internal/middleware/extractor.go b/internal/middleware/extractor.go
index ce7380f..b7db08d 100644
--- a/internal/middleware/extractor.go
+++ b/internal/middleware/extractor.go
@@ -13,11 +13,9 @@ import (
// ValuesExtractor defines a function for extracting values (keys/tokens) from the given context.
type ValuesExtractor func(w http.ResponseWriter, r *http.Request) ([]string, error)
-const (
- // extractorLimit is arbitrary number to limit values extractor can return. this limits possible resource
- // exhaustion attack vector.
- extractorLimit = 20
-)
+// extractorLimit is arbitrary number to limit values extractor can return. this limits possible resource
+// exhaustion attack vector.
+const extractorLimit = 20
var (
errHeaderExtractorValueMissing = errors.New("missing value in request header")
diff --git a/internal/profile/repository/http/http_profile.go b/internal/profile/repository/http/http_profile.go
index f8dadab..ca31fe7 100644
--- a/internal/profile/repository/http/http_profile.go
+++ b/internal/profile/repository/http/http_profile.go
@@ -8,8 +8,11 @@ import (
"net/http"
"net/url"
+ "golang.org/x/exp/slices"
+ "willnorris.com/go/microformats"
+
+ "source.toby3d.me/toby3d/auth/internal/common"
"source.toby3d.me/toby3d/auth/internal/domain"
- "source.toby3d.me/toby3d/auth/internal/httputil"
"source.toby3d.me/toby3d/auth/internal/profile"
)
@@ -17,17 +20,6 @@ type httpProfileRepository struct {
client *http.Client
}
-const (
- ErrPrefix string = "http"
- DefaultMaxRedirectsCount int = 10
-
- hCard string = "h-card"
- propertyEmail string = "email"
- propertyName string = "name"
- propertyPhoto string = "photo"
- propertyURL string = "url"
-)
-
func NewHTPPClientRepository(client *http.Client) profile.Repository {
return &httpProfileRepository{
client: client,
@@ -39,64 +31,69 @@ func (repo *httpProfileRepository) Create(_ context.Context, _ domain.Me, _ doma
return nil
}
-//nolint:cyclop
-func (repo *httpProfileRepository) Get(ctx context.Context, me domain.Me) (*domain.Profile, error) {
+//nolint:cyclop,funlen
+func (repo *httpProfileRepository) Get(_ context.Context, me domain.Me) (*domain.Profile, error) {
resp, err := repo.client.Get(me.String())
if err != nil {
- return nil, fmt.Errorf("%s: cannot fetch user by me: %w", ErrPrefix, err)
+ return nil, fmt.Errorf("%w: cannot fetch user by me: %w", profile.ErrNotExist, err)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
- return nil, fmt.Errorf("cannot read response body: %w", err)
+ return nil, fmt.Errorf("%w: cannot read response body: %w", profile.ErrNotExist, err)
}
- buf := bytes.NewReader(body)
- result := domain.NewProfile()
+ mf2 := microformats.Parse(bytes.NewReader(body), resp.Request.URL)
+ out := new(domain.Profile)
- for _, name := range httputil.ExtractProperty(buf, me.URL(), hCard, propertyName) {
- if n, ok := name.(string); ok {
- result.Name = append(result.Name, n)
- }
- }
-
- for _, rawEmail := range httputil.ExtractProperty(buf, me.URL(), hCard, propertyEmail) {
- email, ok := rawEmail.(string)
- if !ok {
+ for i := range mf2.Items {
+ if !slices.Contains(mf2.Items[i].Type, common.HCard) {
continue
}
- if e, err := domain.ParseEmail(email); err == nil {
- result.Email = append(result.Email, e)
- }
+ parseProfile(mf2.Items[i].Properties, out)
}
- for _, rawURL := range httputil.ExtractProperty(buf, me.URL(), hCard, propertyURL) {
- rawURL, ok := rawURL.(string)
- if !ok {
- continue
- }
-
- if u, err := url.Parse(rawURL); err == nil {
- result.URL = append(result.URL, u)
- }
- }
-
- for _, rawPhoto := range httputil.ExtractProperty(buf, me.URL(), hCard, propertyPhoto) {
- photo, ok := rawPhoto.(string)
- if !ok {
- continue
- }
-
- if p, err := url.Parse(photo); err == nil {
- result.Photo = append(result.Photo, p)
- }
- }
-
- // TODO(toby3d): create method like result.Empty()?
- if result.GetName() == "" && result.GetURL() == nil && result.GetPhoto() == nil && result.GetEmail() == nil {
- return nil, profile.ErrNotExist
- }
-
- return result, nil
+ return out, nil
+}
+
+func parseProfile(src map[string][]any, dst *domain.Profile) {
+ for _, val := range src[common.PropertyName] {
+ v, ok := val.(string)
+ if !ok {
+ continue
+ }
+
+ dst.Name = v
+
+ break
+ }
+
+ for _, val := range src[common.PropertyURL] {
+ v, ok := val.(string)
+ if !ok {
+ continue
+ }
+
+ var err error
+ if dst.URL, err = url.Parse(v); err != nil {
+ continue
+ }
+
+ break
+ }
+
+ for _, val := range src[common.PropertyPhoto] {
+ v, ok := val.(string)
+ if !ok {
+ continue
+ }
+
+ var err error
+ if dst.Photo, err = url.Parse(v); err != nil {
+ continue
+ }
+
+ break
+ }
}
diff --git a/internal/ticket/delivery/http/ticket_http.go b/internal/ticket/delivery/http/ticket_http.go
deleted file mode 100644
index fe6ddaa..0000000
--- a/internal/ticket/delivery/http/ticket_http.go
+++ /dev/null
@@ -1,198 +0,0 @@
-package http
-
-import (
- "fmt"
- "net/http"
-
- "github.com/goccy/go-json"
- "github.com/lestrrat-go/jwx/v2/jwa"
- "golang.org/x/text/language"
- "golang.org/x/text/message"
-
- "source.toby3d.me/toby3d/auth/internal/common"
- "source.toby3d.me/toby3d/auth/internal/domain"
- "source.toby3d.me/toby3d/auth/internal/middleware"
- "source.toby3d.me/toby3d/auth/internal/random"
- "source.toby3d.me/toby3d/auth/internal/ticket"
- "source.toby3d.me/toby3d/auth/internal/urlutil"
- "source.toby3d.me/toby3d/auth/web"
-)
-
-type Handler struct {
- matcher language.Matcher
- tickets ticket.UseCase
- config domain.Config
-}
-
-func NewHandler(tickets ticket.UseCase, matcher language.Matcher, config domain.Config) *Handler {
- return &Handler{
- config: config,
- matcher: matcher,
- tickets: tickets,
- }
-}
-
-func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- //nolint:exhaustivestruct
- chain := middleware.Chain{
- middleware.CSRFWithConfig(middleware.CSRFConfig{
- Skipper: func(_ http.ResponseWriter, r *http.Request) bool {
- head, _ := urlutil.ShiftPath(r.URL.Path)
-
- return r.Method == http.MethodPost && head == "ticket"
- },
- CookieMaxAge: 0,
- CookieSameSite: http.SameSiteStrictMode,
- ContextKey: "csrf",
- CookieDomain: h.config.Server.Domain,
- CookieName: "__Secure-csrf",
- CookiePath: "/ticket",
- TokenLookup: "form:_csrf",
- TokenLength: 0,
- CookieSecure: true,
- CookieHTTPOnly: true,
- }),
- middleware.JWTWithConfig(middleware.JWTConfig{
- AuthScheme: "Bearer",
- BeforeFunc: nil,
- Claims: nil,
- ContextKey: "token",
- ErrorHandler: nil,
- ErrorHandlerWithContext: nil,
- ParseTokenFunc: nil,
- SigningKey: []byte(h.config.JWT.Secret),
- SigningKeys: nil,
- SigningMethod: jwa.SignatureAlgorithm(h.config.JWT.Algorithm),
- Skipper: middleware.DefaultSkipper,
- SuccessHandler: nil,
- TokenLookup: "header:" + common.HeaderAuthorization +
- ",cookie:__Secure-auth-token",
- }),
- }
-
- chain.Handler(h.handleFunc).ServeHTTP(w, r)
-}
-
-func (h *Handler) handleFunc(w http.ResponseWriter, r *http.Request) {
- var head string
- head, r.URL.Path = urlutil.ShiftPath(r.URL.Path)
-
- switch r.Method {
- default:
- http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
- case "", http.MethodGet:
- if head != "" {
- http.NotFound(w, r)
-
- return
- }
-
- h.handleRender(w, r)
- case http.MethodPost:
- switch head {
- default:
- http.NotFound(w, r)
- case "":
- h.handleRedeem(w, r)
- case "send":
- h.handleSend(w, r)
- }
- }
-}
-
-func (h *Handler) handleRender(w http.ResponseWriter, r *http.Request) {
- w.Header().Set(common.HeaderContentType, common.MIMETextHTMLCharsetUTF8)
-
- tags, _, _ := language.ParseAcceptLanguage(r.Header.Get(common.HeaderAcceptLanguage))
- tag, _, _ := h.matcher.Match(tags...)
- baseOf := web.BaseOf{
- Config: &h.config,
- Language: tag,
- Printer: message.NewPrinter(tag),
- }
-
- csrf, _ := r.Context().Value("csrf").([]byte)
- web.WriteTemplate(w, &web.TicketPage{
- BaseOf: baseOf,
- CSRF: csrf,
- })
-}
-
-func (h *Handler) handleSend(w http.ResponseWriter, r *http.Request) {
- w.Header().Set(common.HeaderAccessControlAllowOrigin, h.config.Server.Domain)
- w.Header().Set(common.HeaderContentType, common.MIMEApplicationJSONCharsetUTF8)
-
- encoder := json.NewEncoder(w)
-
- req := new(TicketGenerateRequest)
- if err := req.bind(r); err != nil {
- w.WriteHeader(http.StatusBadRequest)
-
- _ = encoder.Encode(err)
-
- return
- }
-
- ticket := &domain.Ticket{
- Ticket: "",
- Resource: req.Resource.URL,
- Subject: &req.Subject,
- }
-
- var err error
- if ticket.Ticket, err = random.String(h.config.TicketAuth.Length); err != nil {
- w.WriteHeader(http.StatusInternalServerError)
-
- _ = encoder.Encode(domain.NewError(domain.ErrorCodeServerError, err.Error(), ""))
-
- return
- }
-
- if err = h.tickets.Generate(r.Context(), *ticket); err != nil {
- w.WriteHeader(http.StatusInternalServerError)
-
- _ = encoder.Encode(domain.NewError(domain.ErrorCodeServerError, err.Error(), ""))
-
- return
- }
-
- w.WriteHeader(http.StatusOK)
-}
-
-func (h *Handler) handleRedeem(w http.ResponseWriter, r *http.Request) {
- w.Header().Set(common.HeaderContentType, common.MIMEApplicationJSONCharsetUTF8)
-
- encoder := json.NewEncoder(w)
-
- req := new(TicketExchangeRequest)
- if err := req.bind(r); err != nil {
- w.WriteHeader(http.StatusBadRequest)
-
- _ = encoder.Encode(err)
-
- return
- }
-
- token, err := h.tickets.Redeem(r.Context(), domain.Ticket{
- Ticket: req.Ticket,
- Resource: req.Resource.URL,
- Subject: &req.Subject,
- })
- if err != nil {
- w.WriteHeader(http.StatusBadRequest)
-
- _ = encoder.Encode(domain.NewError(domain.ErrorCodeServerError, err.Error(), ""))
-
- return
- }
-
- // TODO(toby3d): print the result as part of the debugging. Instead, we
- // need to send or save the token to the recipient for later use.
- fmt.Fprintf(w, `{
- "access_token": "%s",
- "token_type": "Bearer",
- "scope": "%s",
- "me": "%s"
- }`, token.AccessToken, token.Scope.String(), token.Me.String())
- w.WriteHeader(http.StatusOK)
-}
diff --git a/internal/ticket/delivery/http/ticket_http_schema.go b/internal/ticket/delivery/http/ticket_http_schema.go
deleted file mode 100644
index eb58b29..0000000
--- a/internal/ticket/delivery/http/ticket_http_schema.go
+++ /dev/null
@@ -1,90 +0,0 @@
-package http
-
-import (
- "errors"
- "net/http"
-
- "source.toby3d.me/toby3d/auth/internal/domain"
- "source.toby3d.me/toby3d/form"
-)
-
-type (
- TicketGenerateRequest struct {
- // The access token should be used when acting on behalf of this URL.
- Subject domain.Me `form:"subject"`
-
- // The access token will work at this URL.
- Resource domain.URL `form:"resource"`
- }
-
- TicketExchangeRequest struct {
- // The access token should be used when acting on behalf of this URL.
- Subject domain.Me `form:"subject"`
-
- // The access token will work at this URL.
- Resource domain.URL `form:"resource"`
-
- // A random string that can be redeemed for an access token.
- Ticket string `form:"ticket"`
- }
-)
-
-func (r *TicketGenerateRequest) bind(req *http.Request) error {
- indieAuthError := new(domain.Error)
-
- if err := req.ParseForm(); err != nil {
- return domain.NewError(
- domain.ErrorCodeInvalidRequest,
- err.Error(),
- "https://indieauth.net/source/#authorization-request",
- )
- }
-
- if err := form.Unmarshal([]byte(req.PostForm.Encode()), r); err != nil {
- if errors.As(err, indieAuthError) {
- return indieAuthError
- }
-
- return domain.NewError(
- domain.ErrorCodeInvalidRequest,
- err.Error(),
- "https://indieweb.org/IndieAuth_Ticket_Auth#Create_the_IndieAuth_ticket",
- )
- }
-
- return nil
-}
-
-func (r *TicketExchangeRequest) bind(req *http.Request) error {
- indieAuthError := new(domain.Error)
-
- if err := req.ParseForm(); err != nil {
- return domain.NewError(
- domain.ErrorCodeInvalidRequest,
- err.Error(),
- "https://indieauth.net/source/#authorization-request",
- )
- }
-
- if err := form.Unmarshal([]byte(req.PostForm.Encode()), r); err != nil {
- if errors.As(err, indieAuthError) {
- return indieAuthError
- }
-
- return domain.NewError(
- domain.ErrorCodeInvalidRequest,
- err.Error(),
- "https://indieweb.org/IndieAuth_Ticket_Auth#Create_the_IndieAuth_ticket",
- )
- }
-
- if r.Ticket == "" {
- return domain.NewError(
- domain.ErrorCodeInvalidRequest,
- "ticket parameter is required",
- "https://indieweb.org/IndieAuth_Ticket_Auth#Create_the_IndieAuth_ticket",
- )
- }
-
- return nil
-}
diff --git a/internal/ticket/delivery/http/ticket_http_test.go b/internal/ticket/delivery/http/ticket_http_test.go
deleted file mode 100644
index 718966d..0000000
--- a/internal/ticket/delivery/http/ticket_http_test.go
+++ /dev/null
@@ -1,121 +0,0 @@
-package http_test
-
-/* TODO(toby3d): move CSRF middleware into main
-import (
- "fmt"
- "io"
- "net/http"
- "net/http/httptest"
- "strings"
- "sync"
- "testing"
-
- "golang.org/x/text/language"
- "golang.org/x/text/message"
-
- "source.toby3d.me/toby3d/auth/internal/common"
- "source.toby3d.me/toby3d/auth/internal/domain"
- "source.toby3d.me/toby3d/auth/internal/ticket"
- delivery "source.toby3d.me/toby3d/auth/internal/ticket/delivery/http"
- ticketrepo "source.toby3d.me/toby3d/auth/internal/ticket/repository/memory"
- ucase "source.toby3d.me/toby3d/auth/internal/ticket/usecase"
-)
-
-type Dependencies struct {
- server *httptest.Server
- client *http.Client
- config *domain.Config
- matcher language.Matcher
- store *sync.Map
- ticket *domain.Ticket
- tickets ticket.Repository
- ticketService ticket.UseCase
- token *domain.Token
-}
-
-func TestUpdate(t *testing.T) {
- t.Parallel()
-
- deps := NewDependencies(t)
- t.Cleanup(deps.server.Close)
-
- req := httptest.NewRequest(http.MethodPost, "https://example.com/", strings.NewReader(
- `ticket=`+deps.ticket.Ticket+
- `&resource=`+deps.ticket.Resource.String()+
- `&subject=`+deps.ticket.Subject.String(),
- ))
- req.Header.Set(common.HeaderContentType, common.MIMEApplicationForm)
- deps.token.SetAuthHeader(req)
-
- w := httptest.NewRecorder()
- delivery.NewHandler(deps.ticketService, deps.matcher, *deps.config).
- Handler().
- ServeHTTP(w, req)
-
- domain.TestToken(t).SetAuthHeader(req)
-
- resp := w.Result()
-
- if resp.StatusCode != http.StatusOK &&
- resp.StatusCode != http.StatusAccepted {
- t.Errorf("%s %s = %d, want %d or %d", req.Method, req.RequestURI, resp.StatusCode, http.StatusOK,
- http.StatusAccepted)
- }
-
- // TODO(toby3d): print the result as part of the debugging. Instead, you
- // need to send or save the token to the recipient for later use.
- if resp.Body == nil {
- t.Errorf("%s %s = nil, want not nil", req.Method, req.RequestURI)
- }
-}
-
-func NewDependencies(tb testing.TB) Dependencies {
- tb.Helper()
-
- config := domain.TestConfig(tb)
- matcher := language.NewMatcher(message.DefaultCatalog.Languages())
- store := new(sync.Map)
- ticket := domain.TestTicket(tb)
- token := domain.TestToken(tb)
-
- mux := http.NewServeMux()
- // NOTE(toby3d): private resource
- mux.HandleFunc(ticket.Resource.Path, func(w http.ResponseWriter, r *http.Request) {
- w.Header().Set(common.HeaderContentType, common.MIMETextHTMLCharsetUTF8)
- fmt.Fprintf(w, ``)
- })
- // NOTE(toby3d): token endpoint
- mux.HandleFunc("/token", func(w http.ResponseWriter, r *http.Request) {
- if r.Method != http.MethodPost {
- http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
-
- return
- }
-
- w.Header().Set(common.HeaderContentType, common.MIMEApplicationJSONCharsetUTF8)
- fmt.Fprintf(w, `{
- "access_token": "`+token.AccessToken+`",
- "me": "`+token.Me.String()+`",
- "scope": "`+token.Scope.String()+`",
- "token_type": "Bearer"
- }`)
- })
-
- server := httptest.NewServer(mux)
- client := server.Client()
- tickets := ticketrepo.NewMemoryTicketRepository(store, config)
- ticketService := ucase.NewTicketUseCase(tickets, client, config)
-
- return Dependencies{
- server: server,
- client: client,
- config: config,
- matcher: matcher,
- store: store,
- ticket: ticket,
- tickets: tickets,
- ticketService: ticketService,
- token: token,
- }
-}
-*/
diff --git a/internal/ticket/repository.go b/internal/ticket/repository.go
deleted file mode 100644
index 5e85b52..0000000
--- a/internal/ticket/repository.go
+++ /dev/null
@@ -1,15 +0,0 @@
-package ticket
-
-import (
- "context"
-
- "source.toby3d.me/toby3d/auth/internal/domain"
-)
-
-type Repository interface {
- Create(ctx context.Context, ticket domain.Ticket) error
- GetAndDelete(ctx context.Context, ticket string) (*domain.Ticket, error)
- GC()
-}
-
-var ErrNotExist error = domain.NewError(domain.ErrorCodeInvalidRequest, "ticket not exist or expired", "")
diff --git a/internal/ticket/repository/memory/memory_ticket.go b/internal/ticket/repository/memory/memory_ticket.go
deleted file mode 100644
index c114b52..0000000
--- a/internal/ticket/repository/memory/memory_ticket.go
+++ /dev/null
@@ -1,86 +0,0 @@
-package memory
-
-import (
- "context"
- "sync"
- "time"
-
- "source.toby3d.me/toby3d/auth/internal/domain"
- "source.toby3d.me/toby3d/auth/internal/ticket"
-)
-
-type (
- Ticket struct {
- CreatedAt time.Time
- domain.Ticket
- }
-
- memoryTicketRepository struct {
- mutex *sync.RWMutex
- tickets map[string]Ticket
- config domain.Config
- }
-)
-
-func NewMemoryTicketRepository(config domain.Config) ticket.Repository {
- return &memoryTicketRepository{
- config: config,
- mutex: new(sync.RWMutex),
- tickets: make(map[string]Ticket),
- }
-}
-
-func (repo *memoryTicketRepository) Create(_ context.Context, t domain.Ticket) error {
- repo.mutex.Lock()
- defer repo.mutex.Unlock()
-
- repo.tickets[t.Ticket] = Ticket{
- CreatedAt: time.Now().UTC(),
- Ticket: t,
- }
-
- return nil
-}
-
-func (repo *memoryTicketRepository) GetAndDelete(_ context.Context, t string) (*domain.Ticket, error) {
- repo.mutex.RLock()
-
- out, ok := repo.tickets[t]
- if !ok {
- repo.mutex.RUnlock()
-
- return nil, ticket.ErrNotExist
- }
-
- repo.mutex.RUnlock()
- repo.mutex.Lock()
- delete(repo.tickets, t)
- repo.mutex.Unlock()
-
- return &out.Ticket, nil
-}
-
-func (repo *memoryTicketRepository) GC() {
- ticker := time.NewTicker(time.Second)
- defer ticker.Stop()
-
- for ts := range ticker.C {
- ts = ts.UTC()
-
- repo.mutex.RLock()
-
- for _, t := range repo.tickets {
- if t.CreatedAt.Add(repo.config.Code.Expiry).After(ts) {
- continue
- }
-
- repo.mutex.RUnlock()
- repo.mutex.Lock()
- delete(repo.tickets, t.Ticket.Ticket)
- repo.mutex.Unlock()
- repo.mutex.RLock()
- }
-
- repo.mutex.RUnlock()
- }
-}
diff --git a/internal/ticket/repository/sqlite3/sqlite3_ticket.go b/internal/ticket/repository/sqlite3/sqlite3_ticket.go
deleted file mode 100644
index 7d259e1..0000000
--- a/internal/ticket/repository/sqlite3/sqlite3_ticket.go
+++ /dev/null
@@ -1,122 +0,0 @@
-package sqlite3
-
-import (
- "context"
- "database/sql"
- "errors"
- "fmt"
- "net/url"
- "time"
-
- "github.com/jmoiron/sqlx"
-
- "source.toby3d.me/toby3d/auth/internal/domain"
- "source.toby3d.me/toby3d/auth/internal/ticket"
-)
-
-type (
- Ticket struct {
- Resource string `db:"resource"`
- Subject string `db:"subject"`
- Ticket string `db:"ticket"`
- CreatedAt sql.NullTime `db:"created_at"`
- }
-
- sqlite3TicketRepository struct {
- config *domain.Config
- db *sqlx.DB
- }
-)
-
-const (
- QueryTable string = `CREATE TABLE IF NOT EXISTS tickets (
- created_at DATETIME NOT NULL,
- resource TEXT NOT NULL,
- subject TEXT NOT NULL,
- ticket TEXT UNIQUE PRIMARY KEY NOT NULL
- );`
-
- QueryGet string = `SELECT *
- FROM tickets
- WHERE ticket=$1;`
-
- QueryCreate string = `INSERT INTO tickets (created_at, resource, subject, ticket)
- VALUES (:created_at, :resource, :subject, :ticket);`
-
- QueryDelete string = `DELETE FROM tickets
- WHERE ticket=$1;`
-)
-
-func NewSQLite3TicketRepository(db *sqlx.DB, config *domain.Config) ticket.Repository {
- db.MustExec(QueryTable)
-
- return &sqlite3TicketRepository{
- config: config,
- db: db,
- }
-}
-
-func (repo *sqlite3TicketRepository) Create(ctx context.Context, t domain.Ticket) error {
- if _, err := repo.db.NamedExecContext(ctx, QueryCreate, NewTicket(&t)); err != nil {
- return fmt.Errorf("cannot create token record in db: %w", err)
- }
-
- return nil
-}
-
-func (repo *sqlite3TicketRepository) GetAndDelete(ctx context.Context, rawTicket string) (*domain.Ticket, error) {
- tx, err := repo.db.Beginx()
- if err != nil {
- _ = tx.Rollback()
-
- return nil, fmt.Errorf("failed to begin transaction: %w", err)
- }
-
- tkt := new(Ticket)
- if err = tx.GetContext(ctx, tkt, QueryGet, rawTicket); err != nil {
- //nolint:errcheck // deffered method
- defer tx.Rollback()
-
- if errors.Is(err, sql.ErrNoRows) {
- return nil, ticket.ErrNotExist
- }
-
- return nil, fmt.Errorf("cannot find ticket in db: %w", err)
- }
-
- if _, err = tx.ExecContext(ctx, QueryDelete, rawTicket); err != nil {
- _ = tx.Rollback()
-
- return nil, fmt.Errorf("cannot remove ticket from db: %w", err)
- }
-
- if err = tx.Commit(); err != nil {
- return nil, fmt.Errorf("failed to commit transaction: %w", err)
- }
-
- result := new(domain.Ticket)
-
- tkt.Populate(result)
-
- return result, nil
-}
-
-func (repo *sqlite3TicketRepository) GC() {}
-
-func NewTicket(src *domain.Ticket) *Ticket {
- return &Ticket{
- CreatedAt: sql.NullTime{
- Time: time.Now().UTC(),
- Valid: true,
- },
- Resource: src.Resource.String(),
- Subject: src.Subject.String(),
- Ticket: src.Ticket,
- }
-}
-
-func (t *Ticket) Populate(dst *domain.Ticket) {
- dst.Ticket = t.Ticket
- dst.Subject, _ = domain.ParseMe(t.Subject)
- dst.Resource, _ = url.Parse(t.Resource)
-}
diff --git a/internal/ticket/repository/sqlite3/sqlite3_ticket_test.go b/internal/ticket/repository/sqlite3/sqlite3_ticket_test.go
deleted file mode 100644
index ca2a426..0000000
--- a/internal/ticket/repository/sqlite3/sqlite3_ticket_test.go
+++ /dev/null
@@ -1,82 +0,0 @@
-package sqlite3_test
-
-import (
- "context"
- "regexp"
- "testing"
-
- "github.com/DATA-DOG/go-sqlmock"
-
- "source.toby3d.me/toby3d/auth/internal/domain"
- "source.toby3d.me/toby3d/auth/internal/testing/sqltest"
- repository "source.toby3d.me/toby3d/auth/internal/ticket/repository/sqlite3"
-)
-
-// nolint: gochecknoglobals // slices cannot be contants
-var tableColumns = []string{"created_at", "resource", "subject", "ticket"}
-
-func TestCreate(t *testing.T) {
- t.Parallel()
-
- ticket := domain.TestTicket(t)
- model := repository.NewTicket(ticket)
- db, mock, cleanup := sqltest.Open(t)
- t.Cleanup(cleanup)
-
- createTable(t, mock)
- mock.ExpectExec(regexp.QuoteMeta(`INSERT INTO tickets`)).
- WithArgs(
- sqltest.Time{},
- model.Resource,
- model.Subject,
- model.Ticket,
- ).
- WillReturnResult(sqlmock.NewResult(1, 1))
-
- if err := repository.NewSQLite3TicketRepository(db, domain.TestConfig(t)).
- Create(context.Background(), *ticket); err != nil {
- t.Error(err)
- }
-}
-
-func TestGetAndDelete(t *testing.T) {
- t.Parallel()
-
- ticket := domain.TestTicket(t)
- model := repository.NewTicket(ticket)
- db, mock, cleanup := sqltest.Open(t)
- t.Cleanup(cleanup)
-
- createTable(t, mock)
- mock.ExpectBegin()
- mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM tickets`)).
- WithArgs(model.Ticket).
- WillReturnRows(sqlmock.NewRows(tableColumns).
- AddRow(
- model.CreatedAt.Time,
- model.Resource,
- model.Subject,
- model.Ticket,
- ))
- mock.ExpectExec(regexp.QuoteMeta(`DELETE FROM tickets`)).
- WithArgs(model.Ticket).
- WillReturnResult(sqlmock.NewResult(1, 1))
- mock.ExpectCommit()
-
- result, err := repository.NewSQLite3TicketRepository(db, domain.TestConfig(t)).
- GetAndDelete(context.Background(), ticket.Ticket)
- if err != nil {
- t.Fatal(err)
- }
-
- if result.Ticket != ticket.Ticket {
- t.Errorf("GetAndDelete(%s) = %+v, want %+v", ticket.Ticket, result, ticket)
- }
-}
-
-func createTable(tb testing.TB, mock sqlmock.Sqlmock) {
- tb.Helper()
-
- mock.ExpectExec(regexp.QuoteMeta(repository.QueryTable)).
- WillReturnResult(sqlmock.NewResult(1, 1))
-}
diff --git a/internal/ticket/usecase.go b/internal/ticket/usecase.go
deleted file mode 100644
index 7687bae..0000000
--- a/internal/ticket/usecase.go
+++ /dev/null
@@ -1,24 +0,0 @@
-package ticket
-
-import (
- "context"
-
- "source.toby3d.me/toby3d/auth/internal/domain"
-)
-
-type UseCase interface {
- Generate(ctx context.Context, ticket domain.Ticket) error
-
- // Redeem transform received ticket into access token.
- Redeem(ctx context.Context, ticket domain.Ticket) (*domain.Token, error)
- Exchange(ctx context.Context, ticket string) (*domain.Token, error)
-}
-
-var (
- ErrTicketEndpointNotExist error = domain.NewError(
- domain.ErrorCodeServerError, "ticket_endpoint not found on ticket resource", "",
- )
- ErrTokenEndpointNotExist error = domain.NewError(
- domain.ErrorCodeServerError, "token_endpoint not found on ticket resource", "",
- )
-)
diff --git a/internal/ticket/usecase/ticket_ucase.go b/internal/ticket/usecase/ticket_ucase.go
deleted file mode 100644
index c8ccbe6..0000000
--- a/internal/ticket/usecase/ticket_ucase.go
+++ /dev/null
@@ -1,175 +0,0 @@
-package usecase
-
-import (
- "bytes"
- "context"
- "fmt"
- "io"
- "net/http"
- "net/url"
- "time"
-
- "github.com/goccy/go-json"
-
- "source.toby3d.me/toby3d/auth/internal/common"
- "source.toby3d.me/toby3d/auth/internal/domain"
- "source.toby3d.me/toby3d/auth/internal/httputil"
- "source.toby3d.me/toby3d/auth/internal/ticket"
-)
-
-type (
- //nolint:tagliatelle // https://indieauth.net/source/#access-token-response
- AccessToken struct {
- Me *domain.Me `json:"me"`
- Profile *Profile `json:"profile,omitempty"`
- AccessToken string `json:"access_token"`
- RefreshToken string `json:"refresh_token"`
- ExpiresIn int64 `json:"expires_in,omitempty"`
- }
-
- Profile struct {
- Email *domain.Email `json:"email,omitempty"`
- Photo *domain.URL `json:"photo,omitempty"`
- URL *domain.URL `json:"url,omitempty"`
- Name string `json:"name,omitempty"`
- }
-
- ticketUseCase struct {
- config *domain.Config
- client *http.Client
- tickets ticket.Repository
- }
-)
-
-func NewTicketUseCase(tickets ticket.Repository, client *http.Client, config *domain.Config) ticket.UseCase {
- return &ticketUseCase{
- client: client,
- tickets: tickets,
- config: config,
- }
-}
-
-func (useCase *ticketUseCase) Generate(ctx context.Context, tkt domain.Ticket) error {
- resp, err := useCase.client.Get(tkt.Subject.String())
- if err != nil {
- return fmt.Errorf("cannot discovery ticket subject: %w", err)
- }
-
- body, err := io.ReadAll(resp.Body)
- if err != nil {
- return fmt.Errorf("cannot read response body: %w", err)
- }
-
- buf := bytes.NewReader(body)
- ticketEndpoint := new(url.URL)
-
- // NOTE(toby3d): find metadata first
- metadata, err := httputil.ExtractFromMetadata(useCase.client, tkt.Subject.String())
- if err == nil && metadata != nil {
- ticketEndpoint = metadata.TicketEndpoint
- } else { // NOTE(toby3d): fallback to old links searching
- endpoints := httputil.ExtractEndpoints(buf, tkt.Subject.URL(), resp.Header.Get(common.HeaderLink),
- "ticket_endpoint")
- if len(endpoints) > 0 {
- ticketEndpoint = endpoints[len(endpoints)-1]
- }
- }
-
- if ticketEndpoint == nil {
- return ticket.ErrTicketEndpointNotExist
- }
-
- if err = useCase.tickets.Create(ctx, tkt); err != nil {
- return fmt.Errorf("cannot save ticket in store: %w", err)
- }
-
- payload := make(url.Values)
- payload.Set("ticket", tkt.Ticket)
- payload.Set("subject", tkt.Subject.String())
- payload.Set("resource", tkt.Resource.String())
-
- if _, err = useCase.client.PostForm(ticketEndpoint.String(), payload); err != nil {
- return fmt.Errorf("cannot send ticket to subject ticket_endpoint: %w", err)
- }
-
- return nil
-}
-
-func (useCase *ticketUseCase) Redeem(ctx context.Context, tkt domain.Ticket) (*domain.Token, error) {
- resp, err := useCase.client.Get(tkt.Resource.String())
- if err != nil {
- return nil, fmt.Errorf("cannot discovery ticket resource: %w", err)
- }
-
- body, err := io.ReadAll(resp.Body)
- if err != nil {
- return nil, fmt.Errorf("cannot read response body: %w", err)
- }
-
- buf := bytes.NewReader(body)
- tokenEndpoint := new(url.URL)
-
- // NOTE(toby3d): find metadata first
- metadata, err := httputil.ExtractFromMetadata(useCase.client, tkt.Resource.String())
- if err == nil && metadata != nil {
- tokenEndpoint = metadata.TokenEndpoint
- } else { // NOTE(toby3d): fallback to old links searching
- endpoints := httputil.ExtractEndpoints(buf, tkt.Resource, resp.Header.Get(common.HeaderLink),
- "token_endpoint")
- if len(endpoints) > 0 {
- tokenEndpoint = endpoints[len(endpoints)-1]
- }
- }
-
- if tokenEndpoint == nil || tokenEndpoint.String() == "" {
- return nil, ticket.ErrTokenEndpointNotExist
- }
-
- payload := make(url.Values)
- payload.Set("grant_type", domain.GrantTypeTicket.String())
- payload.Set("ticket", tkt.Ticket)
-
- resp, err = useCase.client.PostForm(tokenEndpoint.String(), payload)
- if err != nil {
- return nil, fmt.Errorf("cannot exchange ticket on token_endpoint: %w", err)
- }
-
- data := new(AccessToken)
- if err := json.NewDecoder(resp.Body).Decode(data); err != nil {
- return nil, fmt.Errorf("cannot unmarshal access token response: %w", err)
- }
-
- return &domain.Token{
- CreatedAt: time.Now().UTC(),
- Expiry: time.Unix(data.ExpiresIn, 0),
- Scope: nil, // TODO(toby3d)
- // TODO(toby3d): should this also include client_id?
- // https://github.com/indieweb/indieauth/issues/85
- ClientID: domain.ClientID{},
- Me: *data.Me,
- AccessToken: data.AccessToken,
- RefreshToken: "", // TODO(toby3d)
- }, nil
-}
-
-func (useCase *ticketUseCase) Exchange(ctx context.Context, ticket string) (*domain.Token, error) {
- tkt, err := useCase.tickets.GetAndDelete(ctx, ticket)
- if err != nil {
- return nil, fmt.Errorf("cannot find provided ticket: %w", err)
- }
-
- token, err := domain.NewToken(domain.NewTokenOptions{
- Expiration: useCase.config.JWT.Expiry,
- Scope: domain.Scopes{domain.ScopeRead},
- Issuer: domain.ClientID{},
- Subject: *tkt.Subject,
- Secret: []byte(useCase.config.JWT.Secret),
- Algorithm: useCase.config.JWT.Algorithm,
- NonceLength: useCase.config.JWT.NonceLength,
- })
- if err != nil {
- return nil, fmt.Errorf("cannot generate a new access token: %w", err)
- }
-
- return token, nil
-}
diff --git a/internal/ticket/usecase/ticket_ucase_test.go b/internal/ticket/usecase/ticket_ucase_test.go
deleted file mode 100644
index 880b84e..0000000
--- a/internal/ticket/usecase/ticket_ucase_test.go
+++ /dev/null
@@ -1,56 +0,0 @@
-package usecase_test
-
-import (
- "context"
- "fmt"
- "net/http"
- "net/http/httptest"
- "net/url"
- "testing"
-
- "source.toby3d.me/toby3d/auth/internal/common"
- "source.toby3d.me/toby3d/auth/internal/domain"
- ucase "source.toby3d.me/toby3d/auth/internal/ticket/usecase"
-)
-
-func TestRedeem(t *testing.T) {
- t.Parallel()
-
- token := domain.TestToken(t)
- ticket := domain.TestTicket(t)
-
- tokenServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- if r.Method != http.MethodPost {
- http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
-
- return
- }
-
- w.Header().Set(common.HeaderContentType, common.MIMEApplicationJSONCharsetUTF8)
- fmt.Fprintf(w, `{
- "token_type": "Bearer",
- "access_token": "%s",
- "scope": "%s",
- "me": "%s"
- }`, token.AccessToken, token.Scope.String(), token.Me.String())
- }))
- t.Cleanup(tokenServer.Close)
-
- subjectServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
- w.Header().Set(common.HeaderContentType, common.MIMETextHTMLCharsetUTF8)
- fmt.Fprint(w, ``)
- }))
- t.Cleanup(subjectServer.Close)
-
- ticket.Resource, _ = url.Parse(subjectServer.URL + "/")
-
- result, err := ucase.NewTicketUseCase(nil, subjectServer.Client(), domain.TestConfig(t)).
- Redeem(context.Background(), *ticket)
- if err != nil {
- t.Fatal(err)
- }
-
- if result.String() != token.String() {
- t.Errorf("Redeem(%+v) = %s, want %s", ticket, result, token)
- }
-}
diff --git a/internal/token/delivery/http/token_http.go b/internal/token/delivery/http/token_http.go
index ee0e995..7325398 100644
--- a/internal/token/delivery/http/token_http.go
+++ b/internal/token/delivery/http/token_http.go
@@ -9,22 +9,19 @@ import (
"source.toby3d.me/toby3d/auth/internal/common"
"source.toby3d.me/toby3d/auth/internal/domain"
"source.toby3d.me/toby3d/auth/internal/middleware"
- "source.toby3d.me/toby3d/auth/internal/ticket"
"source.toby3d.me/toby3d/auth/internal/token"
"source.toby3d.me/toby3d/auth/internal/urlutil"
)
type Handler struct {
- config *domain.Config
- tokens token.UseCase
- tickets ticket.UseCase
+ config domain.Config
+ tokens token.UseCase
}
-func NewHandler(tokens token.UseCase, tickets ticket.UseCase, config *domain.Config) *Handler {
+func NewHandler(tokens token.UseCase, config domain.Config) *Handler {
return &Handler{
- config: config,
- tokens: tokens,
- tickets: tickets,
+ config: config,
+ tokens: tokens,
}
}
@@ -144,13 +141,10 @@ func (h *Handler) handleAction(w http.ResponseWriter, r *http.Request) {
switch action {
case domain.ActionRevoke:
h.handleRevokation(w, r)
- case domain.ActionTicket:
- h.handleTicket(w, r)
}
}
}
-//nolint:funlen
func (h *Handler) handleExchange(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
@@ -190,35 +184,10 @@ func (h *Handler) handleExchange(w http.ResponseWriter, r *http.Request) {
AccessToken: token.AccessToken,
ExpiresIn: token.Expiry.Unix(),
Me: token.Me.String(),
- Profile: nil,
+ Profile: NewTokenProfileResponse(profile),
RefreshToken: "", // TODO(toby3d)
}
- if profile == nil {
- _ = encoder.Encode(resp)
-
- return
- }
-
- resp.Profile = &TokenProfileResponse{
- Name: profile.GetName(),
- URL: "",
- Photo: "",
- Email: "",
- }
-
- if url := profile.GetURL(); url != nil {
- resp.Profile.URL = url.String()
- }
-
- if photo := profile.GetPhoto(); photo != nil {
- resp.Profile.Photo = photo.String()
- }
-
- if email := profile.GetEmail(); email != nil {
- resp.Profile.Email = email.String()
- }
-
_ = encoder.Encode(resp)
w.WriteHeader(http.StatusOK)
@@ -256,44 +225,3 @@ func (h *Handler) handleRevokation(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}
-
-func (h *Handler) handleTicket(w http.ResponseWriter, r *http.Request) {
- if r.Method != http.MethodPost {
- http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
-
- return
- }
-
- w.Header().Set(common.HeaderContentType, common.MIMEApplicationJSONCharsetUTF8)
-
- encoder := json.NewEncoder(w)
-
- req := new(TokenTicketRequest)
- if err := req.bind(r); err != nil {
- w.WriteHeader(http.StatusBadRequest)
-
- _ = encoder.Encode(err)
-
- return
- }
-
- tkn, err := h.tickets.Exchange(r.Context(), req.Ticket)
- if err != nil {
- w.WriteHeader(http.StatusInternalServerError)
-
- _ = encoder.Encode(domain.NewError(domain.ErrorCodeInvalidRequest, err.Error(),
- "https://indieauth.net/source/#request"))
-
- return
- }
-
- _ = encoder.Encode(&TokenExchangeResponse{
- AccessToken: tkn.AccessToken,
- Me: tkn.Me.String(),
- Profile: nil,
- ExpiresIn: tkn.Expiry.Unix(),
- RefreshToken: "", // TODO(toby3d)
- })
-
- w.WriteHeader(http.StatusOK)
-}
diff --git a/internal/token/delivery/http/token_http_schema.go b/internal/token/delivery/http/token_http_schema.go
index 5449aa6..90bac91 100644
--- a/internal/token/delivery/http/token_http_schema.go
+++ b/internal/token/delivery/http/token_http_schema.go
@@ -37,11 +37,6 @@ type (
Token string `form:"token"`
}
- TokenTicketRequest struct {
- Action domain.Action `form:"action"`
- Ticket string `form:"ticket"`
- }
-
TokenIntrospectRequest struct {
Token string `form:"token"`
}
@@ -113,6 +108,35 @@ type (
TokenRevocationResponse struct{}
)
+func NewTokenProfileResponse(in *domain.Profile) *TokenProfileResponse {
+ out := &TokenProfileResponse{
+ Name: "",
+ URL: "",
+ Photo: "",
+ Email: "",
+ }
+
+ if in == nil {
+ return out
+ }
+
+ out.Name = in.Name
+
+ if in.URL != nil {
+ out.URL = in.URL.String()
+ }
+
+ if in.Photo != nil {
+ out.Photo = in.Photo.String()
+ }
+
+ if in.Email != nil {
+ out.Email = in.Email.String()
+ }
+
+ return out
+}
+
func (r *TokenExchangeRequest) bind(req *http.Request) error {
indieAuthError := new(domain.Error)
@@ -165,24 +189,6 @@ func (r *TokenRevocationRequest) bind(req *http.Request) error {
return nil
}
-func (r *TokenTicketRequest) bind(req *http.Request) error {
- indieAuthError := new(domain.Error)
-
- if err := form.Unmarshal([]byte(req.URL.Query().Encode()), r); err != nil {
- if errors.As(err, indieAuthError) {
- return indieAuthError
- }
-
- return domain.NewError(
- domain.ErrorCodeInvalidRequest,
- err.Error(),
- "https://indieauth.net/source/#request",
- )
- }
-
- return nil
-}
-
func (r *TokenIntrospectRequest) bind(req *http.Request) error {
indieAuthError := new(domain.Error)
diff --git a/internal/token/delivery/http/token_http_test.go b/internal/token/delivery/http/token_http_test.go
index be09fce..6079bff 100644
--- a/internal/token/delivery/http/token_http_test.go
+++ b/internal/token/delivery/http/token_http_test.go
@@ -17,9 +17,6 @@ import (
profilerepo "source.toby3d.me/toby3d/auth/internal/profile/repository/memory"
"source.toby3d.me/toby3d/auth/internal/session"
sessionrepo "source.toby3d.me/toby3d/auth/internal/session/repository/memory"
- "source.toby3d.me/toby3d/auth/internal/ticket"
- ticketrepo "source.toby3d.me/toby3d/auth/internal/ticket/repository/memory"
- ticketucase "source.toby3d.me/toby3d/auth/internal/ticket/usecase"
"source.toby3d.me/toby3d/auth/internal/token"
delivery "source.toby3d.me/toby3d/auth/internal/token/delivery/http"
tokenrepo "source.toby3d.me/toby3d/auth/internal/token/repository/memory"
@@ -27,15 +24,13 @@ import (
)
type Dependencies struct {
- client *http.Client
- config *domain.Config
- profiles profile.Repository
- sessions session.Repository
- tickets ticket.Repository
- ticketService ticket.UseCase
- token *domain.Token
- tokens token.Repository
- tokenService token.UseCase
+ client *http.Client
+ config *domain.Config
+ profiles profile.Repository
+ sessions session.Repository
+ token *domain.Token
+ tokens token.Repository
+ tokenService token.UseCase
}
/* TODO(toby3d)
@@ -55,7 +50,7 @@ func TestIntrospection(t *testing.T) {
req.Header.Set(common.HeaderContentType, common.MIMEApplicationForm)
w := httptest.NewRecorder()
- delivery.NewHandler(deps.tokenService, deps.ticketService, deps.config).
+ delivery.NewHandler(deps.tokenService, *deps.config).
ServeHTTP(w, req)
resp := w.Result()
@@ -89,7 +84,7 @@ func TestRevocation(t *testing.T) {
req.Header.Set(common.HeaderAccept, common.MIMEApplicationJSON)
w := httptest.NewRecorder()
- delivery.NewHandler(deps.tokenService, deps.ticketService, deps.config).
+ delivery.NewHandler(deps.tokenService, *deps.config).
ServeHTTP(w, req)
resp := w.Result()
@@ -126,25 +121,21 @@ func NewDependencies(tb testing.TB) Dependencies {
token := domain.TestToken(tb)
profiles := profilerepo.NewMemoryProfileRepository()
sessions := sessionrepo.NewMemorySessionRepository(*config)
- tickets := ticketrepo.NewMemoryTicketRepository(*config)
tokens := tokenrepo.NewMemoryTokenRepository()
- ticketService := ticketucase.NewTicketUseCase(tickets, client, config)
tokenService := tokenucase.NewTokenUseCase(tokenucase.Config{
- Config: config,
+ Config: *config,
Profiles: profiles,
Sessions: sessions,
Tokens: tokens,
})
return Dependencies{
- client: client,
- config: config,
- profiles: profiles,
- sessions: sessions,
- tickets: tickets,
- ticketService: ticketService,
- token: token,
- tokens: tokens,
- tokenService: tokenService,
+ client: client,
+ config: config,
+ profiles: profiles,
+ sessions: sessions,
+ token: token,
+ tokens: tokens,
+ tokenService: tokenService,
}
}
diff --git a/internal/token/usecase/token_ucase.go b/internal/token/usecase/token_ucase.go
index e73f087..ce72e6c 100644
--- a/internal/token/usecase/token_ucase.go
+++ b/internal/token/usecase/token_ucase.go
@@ -16,17 +16,17 @@ import (
type (
Config struct {
- Config *domain.Config
Profiles profile.Repository
Sessions session.Repository
Tokens token.Repository
+ Config domain.Config
}
tokenUseCase struct {
- config *domain.Config
profiles profile.Repository
sessions session.Repository
tokens token.Repository
+ config domain.Config
}
)
@@ -132,7 +132,7 @@ func (uc *tokenUseCase) Verify(ctx context.Context, accessToken string) (*domain
return result, nil, nil //nolint:nilerr // it's okay to return result without profile
}
- if !result.Scope.Has(domain.ScopeEmail) && len(profile.Email) > 0 {
+ if !result.Scope.Has(domain.ScopeEmail) && profile.Email != nil {
profile.Email = nil
}
diff --git a/internal/token/usecase/token_ucase_test.go b/internal/token/usecase/token_ucase_test.go
index 63bfb2c..6860061 100644
--- a/internal/token/usecase/token_ucase_test.go
+++ b/internal/token/usecase/token_ucase_test.go
@@ -45,7 +45,7 @@ func TestExchange(t *testing.T) {
}
tkn, userInfo, err := usecase.NewTokenUseCase(usecase.Config{
- Config: deps.config,
+ Config: *deps.config,
Profiles: deps.profiles,
Sessions: deps.sessions,
Tokens: deps.tokens,
@@ -68,7 +68,7 @@ func TestVerify(t *testing.T) {
deps := NewDependencies(t)
ucase := usecase.NewTokenUseCase(usecase.Config{
- Config: deps.config,
+ Config: *deps.config,
Profiles: deps.profiles,
Sessions: deps.sessions,
Tokens: deps.tokens,
@@ -115,7 +115,7 @@ func TestRevoke(t *testing.T) {
deps := NewDependencies(t)
if err := usecase.NewTokenUseCase(usecase.Config{
- Config: deps.config,
+ Config: *deps.config,
Profiles: deps.profiles,
Sessions: deps.sessions,
Tokens: deps.tokens,
diff --git a/internal/user/delivery/http/user_http.go b/internal/user/delivery/http/user_http.go
index 7f01050..45ab570 100644
--- a/internal/user/delivery/http/user_http.go
+++ b/internal/user/delivery/http/user_http.go
@@ -14,11 +14,11 @@ import (
)
type Handler struct {
- config *domain.Config
+ config domain.Config
tokens token.UseCase
}
-func NewHandler(tokens token.UseCase, config *domain.Config) *Handler {
+func NewHandler(tokens token.UseCase, config domain.Config) *Handler {
return &Handler{
tokens: tokens,
config: config,
@@ -78,8 +78,7 @@ func (h *Handler) handleFunc(w http.ResponseWriter, r *http.Request) {
}
//nolint:errchkjson
- _ = encoder.Encode(NewUserInformationResponse(userInfo,
- userInfo.HasEmail() && tkn.Scope.Has(domain.ScopeEmail)))
+ _ = encoder.Encode(NewUserInformationResponse(userInfo, tkn.Scope.Has(domain.ScopeEmail)))
w.WriteHeader(http.StatusOK)
}
diff --git a/internal/user/delivery/http/user_http_schema.go b/internal/user/delivery/http/user_http_schema.go
index c6a27ff..c10e00e 100644
--- a/internal/user/delivery/http/user_http_schema.go
+++ b/internal/user/delivery/http/user_http_schema.go
@@ -3,10 +3,10 @@ package http
import "source.toby3d.me/toby3d/auth/internal/domain"
type UserInformationResponse struct {
- URL *domain.URL `json:"url,omitempty"`
- Photo *domain.URL `json:"photo,omitempty"`
- Email *domain.Email `json:"email,omitempty"`
- Name string `json:"name,omitempty"`
+ URL string `json:"url,omitempty"`
+ Photo string `json:"photo,omitempty"`
+ Email string `json:"email,omitempty"`
+ Name string `json:"name,omitempty"`
}
func NewUserInformationResponse(in *domain.Profile, hasEmail bool) *UserInformationResponse {
@@ -16,20 +16,18 @@ func NewUserInformationResponse(in *domain.Profile, hasEmail bool) *UserInformat
return out
}
- if in.HasName() {
- out.Name = in.GetName()
+ out.Name = in.Name
+
+ if in.URL != nil {
+ out.URL = in.URL.String()
}
- if in.HasURL() {
- out.URL = &domain.URL{URL: in.GetURL()}
+ if in.Photo != nil {
+ out.Photo = in.Photo.String()
}
- if in.HasPhoto() {
- out.Photo = &domain.URL{URL: in.GetPhoto()}
- }
-
- if hasEmail {
- out.Email = in.GetEmail()
+ if hasEmail && in.Email != nil {
+ out.Email = in.Email.String()
}
return out
diff --git a/internal/user/delivery/http/user_http_test.go b/internal/user/delivery/http/user_http_test.go
index 33526e2..acfeb7d 100644
--- a/internal/user/delivery/http/user_http_test.go
+++ b/internal/user/delivery/http/user_http_test.go
@@ -43,11 +43,10 @@ func TestUserInfo(t *testing.T) {
req.Header.Set(common.HeaderAuthorization, "Bearer "+deps.token.AccessToken)
w := httptest.NewRecorder()
- delivery.NewHandler(deps.tokenService, deps.config).
+ delivery.NewHandler(deps.tokenService, *deps.config).
ServeHTTP(w, req)
resp := w.Result()
-
if exp := http.StatusOK; resp.StatusCode != exp {
t.Errorf("%s %s = %d, want %d", req.Method, req.RequestURI, resp.StatusCode, exp)
}
@@ -57,13 +56,7 @@ func TestUserInfo(t *testing.T) {
t.Fatal(err)
}
- exp := &delivery.UserInformationResponse{
- Name: deps.profile.GetName(),
- URL: &domain.URL{URL: deps.profile.GetURL()},
- Photo: &domain.URL{URL: deps.profile.GetPhoto()},
- Email: deps.profile.GetEmail(),
- }
-
+ exp := delivery.NewUserInformationResponse(deps.profile, true)
if diff := cmp.Diff(result, exp, cmp.AllowUnexported(domain.URL{}, domain.Email{})); diff != "" {
t.Errorf("%s %s = %+v, want %+v", req.Method, req.RequestURI, result, exp)
}
@@ -85,7 +78,7 @@ func NewDependencies(tb testing.TB) Dependencies {
token: domain.TestToken(tb),
tokens: tokens,
tokenService: tokenucase.NewTokenUseCase(tokenucase.Config{
- Config: config,
+ Config: *config,
Profiles: profiles,
Sessions: sessions,
Tokens: tokens,
diff --git a/internal/user/repository/http/http_user.go b/internal/user/repository/http/http_user.go
index 831ac3a..6e57d08 100644
--- a/internal/user/repository/http/http_user.go
+++ b/internal/user/repository/http/http_user.go
@@ -8,34 +8,45 @@ import (
"net/http"
"net/url"
+ "github.com/goccy/go-json"
+ "github.com/tomnomnom/linkheader"
"golang.org/x/exp/slices"
+ "willnorris.com/go/microformats"
"source.toby3d.me/toby3d/auth/internal/common"
"source.toby3d.me/toby3d/auth/internal/domain"
- "source.toby3d.me/toby3d/auth/internal/httputil"
"source.toby3d.me/toby3d/auth/internal/user"
)
-type httpUserRepository struct {
- client *http.Client
-}
+type (
+ //nolint:tagliatelle,lll
+ MetadataResponse struct {
+ TicketEndpoint domain.URL `json:"ticket_endpoint"`
+ AuthorizationEndpoint domain.URL `json:"authorization_endpoint"`
+ IntrospectionEndpoint domain.URL `json:"introspection_endpoint"`
+ RevocationEndpoint domain.URL `json:"revocation_endpoint,omitempty"`
+ ServiceDocumentation domain.URL `json:"service_documentation,omitempty"`
+ TokenEndpoint domain.URL `json:"token_endpoint"`
+ UserinfoEndpoint domain.URL `json:"userinfo_endpoint,omitempty"`
+ Microsub domain.URL `json:"microsub"`
+ Issuer domain.URL `json:"issuer"`
+ Micropub domain.URL `json:"micropub"`
+ GrantTypesSupported []domain.GrantType `json:"grant_types_supported,omitempty"`
+ IntrospectionEndpointAuthMethodsSupported []string `json:"introspection_endpoint_auth_methods_supported,omitempty"`
+ RevocationEndpointAuthMethodsSupported []string `json:"revocation_endpoint_auth_methods_supported,omitempty"`
+ ScopesSupported []domain.Scope `json:"scopes_supported,omitempty"`
+ ResponseTypesSupported []domain.ResponseType `json:"response_types_supported,omitempty"`
+ CodeChallengeMethodsSupported []domain.CodeChallengeMethod `json:"code_challenge_methods_supported"`
+ AuthorizationResponseIssParameterSupported bool `json:"authorization_response_iss_parameter_supported,omitempty"`
+ }
-const (
- DefaultMaxRedirectsCount int = 10
-
- hCard string = "h-card"
- propertyEmail string = "email"
- propertyName string = "name"
- propertyPhoto string = "photo"
- propertyURL string = "url"
- relAuthorizationEndpoint string = "authorization_endpoint"
- relIndieAuthMetadata string = "indieauth-metadata"
- relMicropub string = "micropub"
- relMicrosub string = "microsub"
- relTicketEndpoint string = "ticket_endpoint"
- relTokenEndpoint string = "token_endpoint"
+ httpUserRepository struct {
+ client *http.Client
+ }
)
+const DefaultMaxRedirectsCount int = 10
+
func NewHTTPUserRepository(client *http.Client) user.Repository {
return &httpUserRepository{
client: client,
@@ -47,113 +58,166 @@ func (httpUserRepository) Create(_ context.Context, _ domain.User) error {
return nil
}
+//nolint:funlen
func (repo *httpUserRepository) Get(ctx context.Context, me domain.Me) (*domain.User, error) {
resp, err := repo.client.Get(me.String())
if err != nil {
return nil, fmt.Errorf("cannot fetch user by me: %w", err)
}
- user := &domain.User{
- AuthorizationEndpoint: nil,
- IndieAuthMetadata: nil,
- Me: &me,
- Micropub: nil,
- Microsub: nil,
- Profile: domain.NewProfile(),
- TicketEndpoint: nil,
- TokenEndpoint: nil,
- }
-
- var metadata *domain.Metadata
- if metadata, err = httputil.ExtractFromMetadata(repo.client, me.String()); err == nil {
- user.AuthorizationEndpoint = metadata.AuthorizationEndpoint
- user.Micropub = metadata.MicropubEndpoint
- user.Microsub = metadata.MicrosubEndpoint
- user.TicketEndpoint = metadata.TicketEndpoint
- user.TokenEndpoint = metadata.TokenEndpoint
+ out := &domain.User{
+ Profile: new(domain.Profile),
}
+ // NOTE(toby3d): resolved Me may be different from user-provided Me
+ out.Me, _ = domain.ParseMe(resp.Request.URL.String())
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("cannot read response body: %w", err)
}
- extractUser(me.URL(), user, body, resp.Header.Get(common.HeaderLink))
- extractProfile(me.URL(), user.Profile, body)
+ mf2 := microformats.Parse(bytes.NewReader(body), resp.Request.URL)
- return user, nil
-}
+ // NOTE(toby3d): fetch user profile from nodes
+ for i := range mf2.Items {
+ if !slices.Contains(mf2.Items[i].Type, common.HCard) {
+ continue
+ }
-//nolint:cyclop
-func extractUser(u *url.URL, dst *domain.User, body []byte, header string) {
- for key, target := range map[string]**url.URL{
- relAuthorizationEndpoint: &dst.AuthorizationEndpoint,
- relIndieAuthMetadata: &dst.IndieAuthMetadata,
- relMicropub: &dst.Micropub,
- relMicrosub: &dst.Microsub,
- relTicketEndpoint: &dst.TicketEndpoint,
- relTokenEndpoint: &dst.TokenEndpoint,
+ parseProfile(mf2.Items[i].Properties, out.Profile)
+
+ break
+ }
+
+ // NOTE(toby3d): fetch endpoints from HTML nodes
+ for key, dst := range map[string]**url.URL{
+ common.RelAuthorizationEndpoint: &out.AuthorizationEndpoint,
+ common.RelIndieAuthMetadata: &out.IndieAuthMetadata,
+ common.RelMicropub: &out.Micropub,
+ common.RelMicrosub: &out.Microsub,
+ common.RelTicketEndpoint: &out.TicketEndpoint,
+ common.RelTokenEndpoint: &out.TokenEndpoint,
} {
- if target == nil {
+ vals, ok := mf2.Rels[key]
+ if !ok || len(vals) == 0 {
continue
}
- if endpoints := httputil.ExtractEndpoints(bytes.NewReader(body), u, header, key); len(endpoints) > 0 {
- *target = endpoints[len(endpoints)-1]
+ for i := range vals {
+ var u *url.URL
+ if u, err = url.Parse(vals[i]); err == nil {
+ *dst = u
+
+ break
+ }
}
}
+
+ // NOTE(toby3d): fetch endpoints from Link header
+ for _, link := range linkheader.Parse(resp.Header.Get(common.HeaderLink)) {
+ for key, dst := range map[string]**url.URL{
+ common.RelAuthorizationEndpoint: &out.AuthorizationEndpoint,
+ common.RelIndieAuthMetadata: &out.IndieAuthMetadata,
+ common.RelMicropub: &out.Micropub,
+ common.RelMicrosub: &out.Microsub,
+ common.RelTicketEndpoint: &out.TicketEndpoint,
+ common.RelTokenEndpoint: &out.TokenEndpoint,
+ } {
+ if link.Rel != key {
+ continue
+ }
+
+ var u *url.URL
+ if u, err = url.Parse(link.URL); err == nil {
+ *dst = u
+
+ break
+ }
+ }
+ }
+
+ if out.IndieAuthMetadata == nil {
+ return out, nil
+ }
+
+ // NOTE(toby3d): fetch endpoints from metadata payload
+ if resp, err = repo.client.Get(out.IndieAuthMetadata.String()); err != nil {
+ return out, fmt.Errorf("cannot fetch endpoints from provided metadata URL: %w", err)
+ }
+
+ metadata := new(MetadataResponse)
+ if err = json.NewDecoder(resp.Body).Decode(metadata); err != nil {
+ return out, fmt.Errorf("cannot decode metadata response: %w", err)
+ }
+
+ for src, dst := range map[domain.URL]**url.URL{
+ metadata.AuthorizationEndpoint: &out.AuthorizationEndpoint,
+ metadata.Micropub: &out.Micropub,
+ metadata.Microsub: &out.Microsub,
+ metadata.TicketEndpoint: &out.TicketEndpoint,
+ metadata.TokenEndpoint: &out.TokenEndpoint,
+ } {
+ if src.URL == nil {
+ continue
+ }
+
+ *dst = src.URL
+ }
+
+ return out, nil
}
-//nolint:cyclop
-func extractProfile(u *url.URL, dst *domain.Profile, body []byte) {
- for _, name := range httputil.ExtractProperty(bytes.NewReader(body), u, hCard, propertyName) {
- if n, ok := name.(string); ok && !slices.Contains(dst.Name, n) {
- dst.Name = append(dst.Name, n)
- }
- }
-
- for _, rawEmail := range httputil.ExtractProperty(bytes.NewReader(body), u, hCard, propertyEmail) {
- email, ok := rawEmail.(string)
+func parseProfile(src map[string][]any, dst *domain.Profile) {
+ for _, val := range src[common.PropertyName] {
+ v, ok := val.(string)
if !ok {
continue
}
- if e, err := domain.ParseEmail(email); err == nil && !slices.Contains(dst.Email, e) {
- dst.Email = append(dst.Email, e)
- }
+ dst.Name = v
+
+ break
}
- for _, rawURL := range httputil.ExtractProperty(bytes.NewReader(body), u, hCard, propertyURL) {
- rawURL, ok := rawURL.(string)
+ for _, val := range src[common.PropertyURL] {
+ v, ok := val.(string)
if !ok {
continue
}
- if parsedURL, err := url.Parse(rawURL); err == nil && !containsUrl(dst.URL, u) {
- dst.URL = append(dst.URL, parsedURL)
+ var err error
+ if dst.URL, err = url.Parse(v); err != nil {
+ continue
}
+
+ break
}
- for _, rawPhoto := range httputil.ExtractProperty(bytes.NewReader(body), u, hCard, propertyPhoto) {
- photo, ok := rawPhoto.(string)
+ for _, val := range src[common.PropertyPhoto] {
+ v, ok := val.(string)
if !ok {
continue
}
- if p, err := url.Parse(photo); err == nil && !containsUrl(dst.Photo, p) {
- dst.Photo = append(dst.Photo, p)
- }
- }
-}
-
-func containsUrl(src []*url.URL, find *url.URL) bool {
- for i := range src {
- if src[i].String() != find.String() {
+ var err error
+ if dst.Photo, err = url.Parse(v); err != nil {
continue
}
- return true
+ break
}
- return false
+ for _, val := range src[common.PropertyEmail] {
+ v, ok := val.(string)
+ if !ok {
+ continue
+ }
+
+ var err error
+ if dst.Email, err = domain.ParseEmail(v); err != nil {
+ continue
+ }
+
+ break
+ }
}
diff --git a/internal/user/repository/http/http_user_test.go b/internal/user/repository/http/http_user_test.go
index 34c7aba..510fd0d 100644
--- a/internal/user/repository/http/http_user_test.go
+++ b/internal/user/repository/http/http_user_test.go
@@ -5,6 +5,7 @@ import (
"fmt"
"net/http"
"net/http/httptest"
+ "net/url"
"strings"
"testing"
@@ -39,14 +40,15 @@ func TestGet(t *testing.T) {
t.Parallel()
user := domain.TestUser(t)
+ user.Me = nil
srv := httptest.NewServer(testHandler(t, user))
t.Cleanup(srv.Close)
- user.Me = domain.TestMe(t, srv.URL+"/")
+ user.IndieAuthMetadata, _ = url.Parse(srv.URL + user.IndieAuthMetadata.Path)
result, err := repository.NewHTTPUserRepository(srv.Client()).
- Get(context.Background(), *user.Me)
+ Get(context.Background(), *domain.TestMe(t, srv.URL+"/"))
if err != nil {
t.Fatal(err)
}
@@ -70,12 +72,12 @@ func testHandler(tb testing.TB, user *domain.User) http.Handler {
`<` + user.TokenEndpoint.String() + `>; rel="token_endpoint"`,
}, ", "))
w.Header().Set(common.HeaderContentType, common.MIMETextHTMLCharsetUTF8)
- fmt.Fprintf(w, testBody, user.Name[0], user.URL[0].String(), user.Photo[0].String(), user.Email[0])
+ fmt.Fprintf(w, testBody, user.Name, user.URL, user.Photo, user.Email)
})
mux.HandleFunc(user.IndieAuthMetadata.Path, func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set(common.HeaderContentType, common.MIMEApplicationJSONCharsetUTF8)
fmt.Fprint(w, `{
- "issuer": "`+user.Me.String()+`",
+ "issuer": "https://auth.example.com/",
"authorization_endpoint": "`+user.AuthorizationEndpoint.String()+`",
"token_endpoint": "`+user.TokenEndpoint.String()+`"
}`)
diff --git a/locales/en/messages.gotext.json b/locales/en/messages.gotext.json
index 624594a..3f59650 100644
--- a/locales/en/messages.gotext.json
+++ b/locales/en/messages.gotext.json
@@ -13,7 +13,7 @@
"type": "string",
"underlyingType": "string",
"argNum": 1,
- "expr": "name"
+ "expr": "p.Client.Name"
}
],
"fuzzy": true
diff --git a/locales/en/out.gotext.json b/locales/en/out.gotext.json
index 624594a..3f59650 100644
--- a/locales/en/out.gotext.json
+++ b/locales/en/out.gotext.json
@@ -13,7 +13,7 @@
"type": "string",
"underlyingType": "string",
"argNum": 1,
- "expr": "name"
+ "expr": "p.Client.Name"
}
],
"fuzzy": true
diff --git a/locales/ru/messages.gotext.json b/locales/ru/messages.gotext.json
index d62723b..6e51be6 100644
--- a/locales/ru/messages.gotext.json
+++ b/locales/ru/messages.gotext.json
@@ -13,7 +13,7 @@
"type": "string",
"underlyingType": "string",
"argNum": 1,
- "expr": "name"
+ "expr": "p.Client.Name"
}
]
},
diff --git a/locales/ru/out.gotext.json b/locales/ru/out.gotext.json
index d62723b..6e51be6 100644
--- a/locales/ru/out.gotext.json
+++ b/locales/ru/out.gotext.json
@@ -13,7 +13,7 @@
"type": "string",
"underlyingType": "string",
"argNum": 1,
- "expr": "name"
+ "expr": "p.Client.Name"
}
]
},
diff --git a/main.go b/main.go
index 76fa691..925629d 100644
--- a/main.go
+++ b/main.go
@@ -47,11 +47,6 @@ import (
sessionmemoryrepo "source.toby3d.me/toby3d/auth/internal/session/repository/memory"
sessionsqlite3repo "source.toby3d.me/toby3d/auth/internal/session/repository/sqlite3"
sessionucase "source.toby3d.me/toby3d/auth/internal/session/usecase"
- "source.toby3d.me/toby3d/auth/internal/ticket"
- tickethttpdelivery "source.toby3d.me/toby3d/auth/internal/ticket/delivery/http"
- ticketmemoryrepo "source.toby3d.me/toby3d/auth/internal/ticket/repository/memory"
- ticketsqlite3repo "source.toby3d.me/toby3d/auth/internal/ticket/repository/sqlite3"
- ticketucase "source.toby3d.me/toby3d/auth/internal/ticket/usecase"
"source.toby3d.me/toby3d/auth/internal/token"
tokenhttpdelivery "source.toby3d.me/toby3d/auth/internal/token/delivery/http"
tokenmemoryrepo "source.toby3d.me/toby3d/auth/internal/token/repository/memory"
@@ -67,7 +62,6 @@ type (
clients client.UseCase
matcher language.Matcher
sessions session.UseCase
- tickets ticket.UseCase
profiles profile.UseCase
tokens token.UseCase
static fs.FS
@@ -77,7 +71,6 @@ type (
Client *http.Client
Clients client.Repository
Sessions session.Repository
- Tickets ticket.Repository
Tokens token.Repository
Profiles profile.Repository
Static fs.FS
@@ -94,18 +87,14 @@ var (
// NOTE(toby3d): write logs in stdout, see: https://12factor.net/logs
logger = log.New(os.Stdout, "IndieAuth\t", log.Lmsgprefix|log.LstdFlags|log.LUTC)
// NOTE(toby3d): read configuration from environment, see: https://12factor.net/config
- config = new(domain.Config)
- indieAuthClient = &domain.Client{
- ID: domain.ClientID{},
- Logo: make([]*url.URL, 1),
- RedirectURI: make([]*url.URL, 1),
- URL: make([]*url.URL, 1),
- Name: make([]string, 0),
- }
+ config = new(domain.Config)
)
//nolint:gochecknoglobals
-var cpuProfilePath, memProfilePath string
+var (
+ indieAuthClient *domain.Client
+ cpuProfilePath, memProfilePath string
+)
//go:embed web/static/*
var static embed.FS
@@ -116,31 +105,27 @@ func init() {
flag.StringVar(&memProfilePath, "memprofile", "", "set path to saving pprof memory profile")
flag.Parse()
- if err := env.ParseWithOptions(config, env.Options{Prefix: "AUTH_"}); err != nil {
+ if err := env.ParseWithOptions(config, env.Options{Prefix: "INDIEAUTH_"}); err != nil {
logger.Fatalln(err)
}
// NOTE(toby3d): The server instance itself can be as a client.
- rootURL := config.Server.GetRootURL()
- indieAuthClient.Name = []string{config.Name}
+ rootUrl, err := url.Parse(config.Server.GetRootURL())
+ if err != nil {
+ logger.Fatalln(err)
+ }
- cid, err := domain.ParseClientID(rootURL)
+ cid, err := domain.ParseClientID(rootUrl.String())
if err != nil {
logger.Fatalln("fail to read config:", err)
}
- indieAuthClient.ID = *cid
-
- if indieAuthClient.URL[0], err = url.Parse(rootURL); err != nil {
- logger.Fatalln("cannot parse root URL as client URL:", err)
- }
-
- if indieAuthClient.Logo[0], err = url.Parse(rootURL + "icon.svg"); err != nil {
- logger.Fatalln("cannot parse root URL as client URL:", err)
- }
-
- if indieAuthClient.RedirectURI[0], err = url.Parse(rootURL + "callback"); err != nil {
- logger.Fatalln("cannot parse root URL as client URL:", err)
+ indieAuthClient = &domain.Client{
+ Logo: rootUrl.JoinPath("icon.svg"),
+ URL: rootUrl,
+ ID: *cid,
+ Name: config.Name,
+ RedirectURI: []*url.URL{rootUrl.JoinPath("callback")},
}
}
@@ -159,7 +144,6 @@ func main() {
default:
opts.Tokens = tokenmemoryrepo.NewMemoryTokenRepository()
opts.Sessions = sessionmemoryrepo.NewMemorySessionRepository(*config)
- opts.Tickets = ticketmemoryrepo.NewMemoryTicketRepository(*config)
case "sqlite3":
store, err := sqlx.Open("sqlite", config.Database.Path)
if err != nil {
@@ -172,7 +156,6 @@ func main() {
opts.Tokens = tokensqlite3repo.NewSQLite3TokenRepository(store)
opts.Sessions = sessionsqlite3repo.NewSQLite3SessionRepository(store)
- opts.Tickets = ticketsqlite3repo.NewSQLite3TicketRepository(store, config)
}
go opts.Sessions.GC()
@@ -180,9 +163,7 @@ func main() {
opts.Client = new(http.Client)
opts.Clients = clienthttprepo.NewHTTPClientRepository(opts.Client)
opts.Profiles = profilehttprepo.NewHTPPClientRepository(opts.Client)
-
app := NewApp(opts)
-
server := &http.Server{
Addr: config.Server.GetAddress(),
BaseContext: nil,
@@ -219,7 +200,12 @@ func main() {
logger.Printf("started at %s, available at %s", config.Server.GetAddress(),
config.Server.GetRootURL())
- err = server.ListenAndServe()
+ if config.Server.CertificateFile != "" && config.Server.KeyFile != "" {
+ err = server.ListenAndServeTLS(config.Server.CertificateFile, config.Server.KeyFile)
+ } else {
+ err = server.ListenAndServe()
+ }
+
if err != nil && !errors.Is(err, http.ErrServerClosed) {
logger.Fatalln("cannot listen and serve:", err)
}
@@ -251,14 +237,13 @@ func main() {
func NewApp(opts NewAppOptions) *App {
return &App{
static: opts.Static,
- auth: authucase.NewAuthUseCase(opts.Sessions, opts.Profiles, config),
+ auth: authucase.NewAuthUseCase(opts.Sessions, opts.Profiles, *config),
clients: clientucase.NewClientUseCase(opts.Clients),
matcher: language.NewMatcher(message.DefaultCatalog.Languages()),
profiles: profileucase.NewProfileUseCase(opts.Profiles),
sessions: sessionucase.NewSessionUseCase(opts.Sessions),
- tickets: ticketucase.NewTicketUseCase(opts.Tickets, opts.Client, config),
tokens: tokenucase.NewTokenUseCase(tokenucase.Config{
- Config: config,
+ Config: *config,
Profiles: opts.Profiles,
Sessions: opts.Sessions,
Tokens: opts.Tokens,
@@ -327,15 +312,14 @@ func (app *App) Handler() http.Handler {
Matcher: app.matcher,
Profiles: app.profiles,
})
- token := tokenhttpdelivery.NewHandler(app.tokens, app.tickets, config)
+ token := tokenhttpdelivery.NewHandler(app.tokens, *config)
client := clienthttpdelivery.NewHandler(clienthttpdelivery.NewHandlerOptions{
Client: *indieAuthClient,
Config: *config,
Matcher: app.matcher,
Tokens: app.tokens,
})
- user := userhttpdelivery.NewHandler(app.tokens, config)
- ticket := tickethttpdelivery.NewHandler(app.tickets, app.matcher, *config)
+ user := userhttpdelivery.NewHandler(app.tokens, *config)
staticHandler := http.FileServer(http.FS(app.static))
return http.HandlerFunc(middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -368,10 +352,6 @@ func (app *App) Handler() http.Handler {
r.URL.Path = tail
user.ServeHTTP(w, r)
- case "ticket":
- r.URL.Path = tail
-
- ticket.ServeHTTP(w, r)
}
}).Intercept(middleware.LogFmt()))
}
diff --git a/web/authorize.qtpl b/web/authorize.qtpl
index 17330ba..3158500 100644
--- a/web/authorize.qtpl
+++ b/web/authorize.qtpl
@@ -17,8 +17,8 @@
} %}
{% func (p *AuthorizePage) title() %}
-{% if name := p.Client.GetName(); name != "" %}
-{%= p.t("Authorize %s", name) %}
+{% if p.Client.Name != "" %}
+{%= p.t("Authorize %s", p.Client.Name) %}
{% else %}
{%= p.t("Authorize application") %}
{% endif %}
@@ -26,7 +26,7 @@
{% func (p *AuthorizePage) body() %}
- {% if p.Client.GetLogo() != nil %}
+ {% if p.Client.Logo != nil %}
{% endif %}
diff --git a/web/authorize.qtpl.go b/web/authorize.qtpl.go
index 055d637..28cb8a8 100644
--- a/web/authorize.qtpl.go
+++ b/web/authorize.qtpl.go
@@ -43,12 +43,12 @@ func (p *AuthorizePage) streamtitle(qw422016 *qt422016.Writer) {
qw422016.N().S(`
`)
//line web/authorize.qtpl:20
- if name := p.Client.GetName(); name != "" {
+ if p.Client.Name != "" {
//line web/authorize.qtpl:20
qw422016.N().S(`
`)
//line web/authorize.qtpl:21
- p.streamt(qw422016, "Authorize %s", name)
+ p.streamt(qw422016, "Authorize %s", p.Client.Name)
//line web/authorize.qtpl:21
qw422016.N().S(`
`)
@@ -103,7 +103,7 @@ func (p *AuthorizePage) streambody(qw422016 *qt422016.Writer) {
`)
//line web/authorize.qtpl:29
- if p.Client.GetLogo() != nil {
+ if p.Client.Logo != nil {
//line web/authorize.qtpl:29
qw422016.N().S(`
@@ -133,12 +133,12 @@ func (p *AuthorizePage) streambody(qw422016 *qt422016.Writer) {