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 %} {%s p.Client.GetName() %} {% endif %}

- {% if p.Client.GetURL() != nil %} - + {% if p.Client.URL != nil %} + {% endif %} - {% if p.Client.GetName() != "" %} - {%s p.Client.GetName() %} + {% if p.Client.Name != "" %} + {%s p.Client.Name %} {% else %} {%s p.Client.ID.String() %} {% endif %} - {% if p.Client.GetURL() != nil %} + {% if p.Client.URL != 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(` `)
 //line web/authorize.qtpl:38
-		qw422016.E().S(p.Client.GetName())
+		qw422016.E().S(p.Client.Name)
 //line web/authorize.qtpl:38
 		qw422016.N().S(` @@ -133,12 +133,12 @@ func (p *AuthorizePage) streambody(qw422016 *qt422016.Writer) {

`) //line web/authorize.qtpl:43 - if p.Client.GetURL() != nil { + if p.Client.URL != nil { //line web/authorize.qtpl:43 qw422016.N().S(` `) @@ -148,12 +148,12 @@ func (p *AuthorizePage) streambody(qw422016 *qt422016.Writer) { qw422016.N().S(` `) //line web/authorize.qtpl:46 - if p.Client.GetName() != "" { + if p.Client.Name != "" { //line web/authorize.qtpl:46 qw422016.N().S(` `) //line web/authorize.qtpl:47 - qw422016.E().S(p.Client.GetName()) + qw422016.E().S(p.Client.Name) //line web/authorize.qtpl:47 qw422016.N().S(` `) @@ -173,7 +173,7 @@ func (p *AuthorizePage) streambody(qw422016 *qt422016.Writer) { qw422016.N().S(` `) //line web/authorize.qtpl:51 - if p.Client.GetURL() != nil { + if p.Client.URL != nil { //line web/authorize.qtpl:51 qw422016.N().S(` diff --git a/web/home.qtpl b/web/home.qtpl index b71d167..39e724f 100644 --- a/web/home.qtpl +++ b/web/home.qtpl @@ -19,21 +19,23 @@ {% func (p *HomePage) body() %}
+ {% if p.Client.Logo != nil %} + {% endif %}

+ href="{%s p.Client.URL.String() %}"> - {%s p.Client.GetName() %} + {%s p.Client.Name %}

diff --git a/web/home.qtpl.go b/web/home.qtpl.go index e8572b8..4866844 100644 --- a/web/home.qtpl.go +++ b/web/home.qtpl.go @@ -81,24 +81,32 @@ func (p *HomePage) head() string { //line web/home.qtpl:20 func (p *HomePage) streambody(qw422016 *qt422016.Writer) { //line web/home.qtpl:20 - qw422016.N().S(`
`) +//line web/home.qtpl:22 + if p.Client.Logo != nil { +//line web/home.qtpl:22 + qw422016.N().S(`

`) +//line web/home.qtpl:32 + } +//line web/home.qtpl:32 + qw422016.N().S(`

`) -//line web/home.qtpl:36 - qw422016.E().S(p.Client.GetName()) -//line web/home.qtpl:36 +//line web/home.qtpl:38 + qw422016.E().S(p.Client.Name) +//line web/home.qtpl:38 qw422016.N().S(`

`) -//line web/home.qtpl:49 +//line web/home.qtpl:51 for name, value := range map[string]string{ "client_id": p.Client.ID.String(), "redirect_uri": p.Client.RedirectURI[0].String(), @@ -106,49 +114,49 @@ func (p *HomePage) streambody(qw422016 *qt422016.Writer) { "scope": domain.Scopes{domain.ScopeEmail, domain.ScopeProfile}.String(), "state": p.State, } { -//line web/home.qtpl:55 +//line web/home.qtpl:57 qw422016.N().S(` `) -//line web/home.qtpl:59 +//line web/home.qtpl:61 } -//line web/home.qtpl:59 +//line web/home.qtpl:61 qw422016.N().S(`
`) -//line web/home.qtpl:71 +//line web/home.qtpl:73 } -//line web/home.qtpl:71 +//line web/home.qtpl:73 func (p *HomePage) writebody(qq422016 qtio422016.Writer) { -//line web/home.qtpl:71 +//line web/home.qtpl:73 qw422016 := qt422016.AcquireWriter(qq422016) -//line web/home.qtpl:71 +//line web/home.qtpl:73 p.streambody(qw422016) -//line web/home.qtpl:71 +//line web/home.qtpl:73 qt422016.ReleaseWriter(qw422016) -//line web/home.qtpl:71 +//line web/home.qtpl:73 } -//line web/home.qtpl:71 +//line web/home.qtpl:73 func (p *HomePage) body() string { -//line web/home.qtpl:71 +//line web/home.qtpl:73 qb422016 := qt422016.AcquireByteBuffer() -//line web/home.qtpl:71 +//line web/home.qtpl:73 p.writebody(qb422016) -//line web/home.qtpl:71 +//line web/home.qtpl:73 qs422016 := string(qb422016.B) -//line web/home.qtpl:71 +//line web/home.qtpl:73 qt422016.ReleaseByteBuffer(qb422016) -//line web/home.qtpl:71 +//line web/home.qtpl:73 return qs422016 -//line web/home.qtpl:71 +//line web/home.qtpl:73 }