♻️ Refactored ticket package
This commit is contained in:
parent
b80fd68b3d
commit
143bc65d3b
|
@ -7,7 +7,6 @@ import (
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
http "github.com/valyala/fasthttp"
|
|
||||||
|
|
||||||
"source.toby3d.me/website/oauth/internal/domain"
|
"source.toby3d.me/website/oauth/internal/domain"
|
||||||
)
|
)
|
||||||
|
@ -67,17 +66,22 @@ func initConfig() {
|
||||||
log.Fatalln("fail to read config:", err)
|
log.Fatalln("fail to read config:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
u, logo, redirect := http.AcquireURI(), http.AcquireURI(), http.AcquireURI()
|
url, err := domain.NewURL(rootURL)
|
||||||
if err = u.Parse(nil, []byte(rootURL)); err != nil {
|
if err != nil {
|
||||||
log.Fatalln("cannot parse client URL:", err)
|
log.Fatalln("cannot parse root URL as client URL:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
u.CopyTo(logo)
|
logo, err := domain.NewURL(rootURL + config.Server.StaticURLPrefix + "/icon.svg")
|
||||||
u.CopyTo(redirect)
|
if err != nil {
|
||||||
redirect.SetPath("/callback")
|
log.Fatalln("cannot parse root URL as client URL:", err)
|
||||||
logo.SetPath(config.Server.StaticURLPrefix + "/icon.svg")
|
}
|
||||||
|
|
||||||
client.URL = []*http.URI{u}
|
redirectURI, err := domain.NewURL(rootURL + "/callback")
|
||||||
client.Logo = []*http.URI{logo}
|
if err != nil {
|
||||||
client.RedirectURI = []*http.URI{redirect}
|
log.Fatalln("cannot parse root URL as client URL:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
client.URL = []*domain.URL{url}
|
||||||
|
client.Logo = []*domain.URL{logo}
|
||||||
|
client.RedirectURI = []*domain.URL{redirectURI}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,14 +21,11 @@ import (
|
||||||
"golang.org/x/text/message"
|
"golang.org/x/text/message"
|
||||||
|
|
||||||
clienthttpdelivery "source.toby3d.me/website/oauth/internal/client/delivery/http"
|
clienthttpdelivery "source.toby3d.me/website/oauth/internal/client/delivery/http"
|
||||||
clientrepo "source.toby3d.me/website/oauth/internal/client/repository/http"
|
|
||||||
clientucase "source.toby3d.me/website/oauth/internal/client/usecase"
|
|
||||||
healthhttpdelivery "source.toby3d.me/website/oauth/internal/health/delivery/http"
|
healthhttpdelivery "source.toby3d.me/website/oauth/internal/health/delivery/http"
|
||||||
metadatahttpdelivery "source.toby3d.me/website/oauth/internal/metadata/delivery/http"
|
metadatahttpdelivery "source.toby3d.me/website/oauth/internal/metadata/delivery/http"
|
||||||
tickethttpdelivery "source.toby3d.me/website/oauth/internal/ticket/delivery/http"
|
tickethttpdelivery "source.toby3d.me/website/oauth/internal/ticket/delivery/http"
|
||||||
|
ticketrepo "source.toby3d.me/website/oauth/internal/ticket/repository/http"
|
||||||
ticketucase "source.toby3d.me/website/oauth/internal/ticket/usecase"
|
ticketucase "source.toby3d.me/website/oauth/internal/ticket/usecase"
|
||||||
userrepo "source.toby3d.me/website/oauth/internal/user/repository/http"
|
|
||||||
userucase "source.toby3d.me/website/oauth/internal/user/usecase"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -84,9 +81,15 @@ func startServer(cmd *cobra.Command, args []string) {
|
||||||
healthhttpdelivery.NewRequestHandler().Register(r)
|
healthhttpdelivery.NewRequestHandler().Register(r)
|
||||||
metadatahttpdelivery.NewRequestHandler(config).Register(r)
|
metadatahttpdelivery.NewRequestHandler(config).Register(r)
|
||||||
clienthttpdelivery.NewRequestHandler(config, client, matcher).Register(r)
|
clienthttpdelivery.NewRequestHandler(config, client, matcher).Register(r)
|
||||||
|
/*
|
||||||
|
serverhttpdelivery.NewRequestHandler(serverhttpdelivery.Config{
|
||||||
|
Config: config,
|
||||||
|
Clients: clientucase.NewClientUseCase(clientrepo.NewHTTPClientRepository(httpClient)),
|
||||||
|
Matcher: matcher,
|
||||||
|
}).Register(r)
|
||||||
|
*/
|
||||||
tickethttpdelivery.NewRequestHandler(
|
tickethttpdelivery.NewRequestHandler(
|
||||||
ticketucase.NewTicketUseCase(httpClient),
|
ticketucase.NewTicketUseCase(ticketrepo.NewHTTPTicketRepository(httpClient), httpClient),
|
||||||
userucase.NewUserUseCase(userrepo.NewHTTPUserRepository(httpClient)),
|
|
||||||
).Register(r)
|
).Register(r)
|
||||||
|
|
||||||
if enablePprof {
|
if enablePprof {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package domain
|
package domain
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
@ -59,3 +60,16 @@ func (u *URL) UnmarshalJSON(v []byte) error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *URL) URL() *url.URL {
|
||||||
|
if u.URI == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := url.ParseRequestURI(u.URI.String())
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
|
@ -12,7 +12,6 @@ import (
|
||||||
"source.toby3d.me/website/oauth/internal/common"
|
"source.toby3d.me/website/oauth/internal/common"
|
||||||
"source.toby3d.me/website/oauth/internal/domain"
|
"source.toby3d.me/website/oauth/internal/domain"
|
||||||
"source.toby3d.me/website/oauth/internal/ticket"
|
"source.toby3d.me/website/oauth/internal/ticket"
|
||||||
"source.toby3d.me/website/oauth/internal/user"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
@ -24,19 +23,18 @@ type (
|
||||||
Resource *domain.URL `form:"resource"`
|
Resource *domain.URL `form:"resource"`
|
||||||
|
|
||||||
// The access token should be used when acting on behalf of this URL.
|
// The access token should be used when acting on behalf of this URL.
|
||||||
|
// WARN(toby3d): deadcode for now
|
||||||
Subject *domain.Me `form:"subject"`
|
Subject *domain.Me `form:"subject"`
|
||||||
}
|
}
|
||||||
|
|
||||||
RequestHandler struct {
|
RequestHandler struct {
|
||||||
users user.UseCase
|
useCase ticket.UseCase
|
||||||
tickets ticket.UseCase
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewRequestHandler(tickets ticket.UseCase, users user.UseCase) *RequestHandler {
|
func NewRequestHandler(useCase ticket.UseCase) *RequestHandler {
|
||||||
return &RequestHandler{
|
return &RequestHandler{
|
||||||
tickets: tickets,
|
useCase: useCase,
|
||||||
users: users,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,20 +56,11 @@ func (h *RequestHandler) update(ctx *http.RequestCtx) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(toby3d): fetch token endpoint on Resource URL instead
|
token, err := h.useCase.Redeem(ctx, &domain.Ticket{
|
||||||
u, err := h.users.Fetch(ctx, req.Subject)
|
Ticket: req.Ticket,
|
||||||
if err != nil {
|
Resource: req.Resource,
|
||||||
ctx.SetStatusCode(http.StatusBadRequest)
|
Subject: req.Subject,
|
||||||
encoder.Encode(domain.Error{
|
})
|
||||||
Code: "invalid_request",
|
|
||||||
Description: err.Error(),
|
|
||||||
Frame: xerrors.Caller(1),
|
|
||||||
})
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
token, err := h.tickets.Redeem(ctx, u.TokenEndpoint, req.Ticket)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.SetStatusCode(http.StatusBadRequest)
|
ctx.SetStatusCode(http.StatusBadRequest)
|
||||||
encoder.Encode(domain.Error{
|
encoder.Encode(domain.Error{
|
||||||
|
|
|
@ -15,9 +15,8 @@ import (
|
||||||
"source.toby3d.me/website/oauth/internal/domain"
|
"source.toby3d.me/website/oauth/internal/domain"
|
||||||
"source.toby3d.me/website/oauth/internal/testing/httptest"
|
"source.toby3d.me/website/oauth/internal/testing/httptest"
|
||||||
delivery "source.toby3d.me/website/oauth/internal/ticket/delivery/http"
|
delivery "source.toby3d.me/website/oauth/internal/ticket/delivery/http"
|
||||||
|
ticketrepo "source.toby3d.me/website/oauth/internal/ticket/repository/memory"
|
||||||
ucase "source.toby3d.me/website/oauth/internal/ticket/usecase"
|
ucase "source.toby3d.me/website/oauth/internal/ticket/usecase"
|
||||||
userrepo "source.toby3d.me/website/oauth/internal/user/repository/memory"
|
|
||||||
userucase "source.toby3d.me/website/oauth/internal/user/usecase"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO(toby3d): looks ugly, refactor this?
|
// TODO(toby3d): looks ugly, refactor this?
|
||||||
|
@ -30,7 +29,10 @@ func TestUpdate(t *testing.T) {
|
||||||
token := domain.TestToken(t)
|
token := domain.TestToken(t)
|
||||||
|
|
||||||
store := new(sync.Map)
|
store := new(sync.Map)
|
||||||
store.Store(path.Join(userrepo.DefaultPathPrefix, ticket.Subject.String()), domain.TestUser(t))
|
store.Store(
|
||||||
|
path.Join(ticketrepo.DefaultPathPrefix, ticket.Resource.String()),
|
||||||
|
domain.TestURL(t, "https://example.com/token"),
|
||||||
|
)
|
||||||
|
|
||||||
userClient, _, userCleanup := httptest.New(t, func(ctx *http.RequestCtx) {
|
userClient, _, userCleanup := httptest.New(t, func(ctx *http.RequestCtx) {
|
||||||
ctx.SuccessString(common.MIMEApplicationJSONCharsetUTF8, fmt.Sprintf(`{
|
ctx.SuccessString(common.MIMEApplicationJSONCharsetUTF8, fmt.Sprintf(`{
|
||||||
|
@ -47,9 +49,8 @@ func TestUpdate(t *testing.T) {
|
||||||
client, _, cleanup := httptest.New(t, r.Handler)
|
client, _, cleanup := httptest.New(t, r.Handler)
|
||||||
t.Cleanup(cleanup)
|
t.Cleanup(cleanup)
|
||||||
|
|
||||||
delivery.NewRequestHandler(
|
delivery.NewRequestHandler(ucase.NewTicketUseCase(ticketrepo.NewMemoryTicketRepository(store), userClient)).
|
||||||
ucase.NewTicketUseCase(userClient), userucase.NewUserUseCase(userrepo.NewMemoryUserRepository(store)),
|
Register(r)
|
||||||
).Register(r)
|
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "https://example.com/ticket", []byte(
|
req := httptest.NewRequest(http.MethodPost, "https://example.com/ticket", []byte(
|
||||||
`ticket=`+ticket.Ticket+
|
`ticket=`+ticket.Ticket+
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
package ticket
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"source.toby3d.me/website/oauth/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Repository interface {
|
||||||
|
// Get returns token endpoint founded by resource URL.
|
||||||
|
Get(ctx context.Context, resource *domain.URL) (*domain.URL, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrNotExist = errors.New("token_endpoint not found on resource URL")
|
|
@ -0,0 +1,174 @@
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/tomnomnom/linkheader"
|
||||||
|
http "github.com/valyala/fasthttp"
|
||||||
|
"willnorris.com/go/microformats"
|
||||||
|
|
||||||
|
"source.toby3d.me/website/oauth/internal/domain"
|
||||||
|
"source.toby3d.me/website/oauth/internal/ticket"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
//nolint: tagliatelle
|
||||||
|
Response struct {
|
||||||
|
// The server's issuer identifier. The issuer identifier is a
|
||||||
|
// URL that uses the "https" scheme and has no query or fragment
|
||||||
|
// components. The identifier MUST be a prefix of the
|
||||||
|
// indieauth-metadata URL. e.g. for an indieauth-metadata
|
||||||
|
// endpoint
|
||||||
|
// https://example.com/.well-known/oauth-authorization-server,
|
||||||
|
// the issuer URL could be https://example.com/, or for a
|
||||||
|
// metadata URL of
|
||||||
|
// https://example.com/wp-json/indieauth/1.0/metadata, the
|
||||||
|
// issuer URL could be https://example.com/wp-json/indieauth/1.0
|
||||||
|
Issuer *domain.URL `json:"issuer"`
|
||||||
|
|
||||||
|
// The Authorization Endpoint.
|
||||||
|
AuthorizationEndpoint *domain.URL `json:"authorization_endpoint"`
|
||||||
|
|
||||||
|
// The Token Endpoint.
|
||||||
|
TokenEndpoint *domain.URL `json:"token_endpoint"`
|
||||||
|
|
||||||
|
// JSON array containing scope values supported by the
|
||||||
|
// IndieAuth server. Servers MAY choose not to advertise some
|
||||||
|
// supported scope values even when this parameter is used.
|
||||||
|
ScopesSupported domain.Scopes `json:"scopes_supported,omitempty"`
|
||||||
|
|
||||||
|
// JSON array containing the response_type values supported.
|
||||||
|
// This differs from RFC8414 in that this parameter is OPTIONAL
|
||||||
|
// and that, if omitted, the default is code.
|
||||||
|
ResponseTypesSupported []domain.ResponseType `json:"response_types_supported,omitempty"`
|
||||||
|
|
||||||
|
// JSON array containing grant type values supported. If
|
||||||
|
// omitted, the default value differs from RFC8414 and is
|
||||||
|
// authorization_code.
|
||||||
|
GrantTypesSupported []domain.GrantType `json:"grant_types_supported,omitempty"`
|
||||||
|
|
||||||
|
// URL of a page containing human-readable information that
|
||||||
|
// developers might need to know when using the server. This
|
||||||
|
// might be a link to the IndieAuth spec or something more
|
||||||
|
// personal to your implementation.
|
||||||
|
ServiceDocumentation *domain.URL `json:"service_documentation,omitempty"`
|
||||||
|
|
||||||
|
// JSON array containing the methods supported for PKCE. This
|
||||||
|
// parameter differs from RFC8414 in that it is not optional as
|
||||||
|
// PKCE is REQUIRED.
|
||||||
|
CodeChallengeMethodsSupported []domain.CodeChallengeMethod `json:"code_challenge_methods_supported"`
|
||||||
|
|
||||||
|
// Boolean parameter indicating whether the authorization server
|
||||||
|
// provides the iss parameter. If omitted, the default value is
|
||||||
|
// false. As the iss parameter is REQUIRED, this is provided for
|
||||||
|
// compatibility with OAuth 2.0 servers implementing the
|
||||||
|
// parameter.
|
||||||
|
//
|
||||||
|
//nolint: lll
|
||||||
|
AuthorizationResponseIssParameterSupported bool `json:"authorization_response_iss_parameter_supported,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
httpTicketRepository struct {
|
||||||
|
client *http.Client
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
relIndieAuthMetadata string = "indieauth-metadata"
|
||||||
|
relTokenEndpoint string = "token_endpoint"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewHTTPTicketRepository(client *http.Client) ticket.Repository {
|
||||||
|
return &httpTicketRepository{
|
||||||
|
client: client,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *httpTicketRepository) Get(_ context.Context, resource *domain.URL) (*domain.URL, error) {
|
||||||
|
req := http.AcquireRequest()
|
||||||
|
defer http.ReleaseRequest(req)
|
||||||
|
req.Header.SetMethod(http.MethodGet)
|
||||||
|
req.SetRequestURIBytes(resource.FullURI())
|
||||||
|
|
||||||
|
resp := http.AcquireResponse()
|
||||||
|
defer http.ReleaseResponse(resp)
|
||||||
|
|
||||||
|
if err := repo.client.Do(req, resp); err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot fetch resource URL: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if metadataEndpoint := extractEndpoint(resp, relIndieAuthMetadata); metadataEndpoint != nil {
|
||||||
|
if result, err := extractFromMetadata(repo.client, metadataEndpoint); err == nil && result != nil {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if result := extractEndpoint(resp, relTokenEndpoint); result != nil {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, ticket.ErrNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractEndpoint(resp *http.Response, name string) *domain.URL {
|
||||||
|
u, err := extractEndpointFromHeader(resp, name)
|
||||||
|
if err == nil && u != nil {
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
if u, err = extractEndpointFromBody(resp, name); err == nil && u != nil {
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractEndpointFromHeader(resp *http.Response, name string) (*domain.URL, error) {
|
||||||
|
for _, link := range linkheader.Parse(string(resp.Header.Peek(http.HeaderLink))) {
|
||||||
|
if !strings.EqualFold(link.Rel, name) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
u := http.AcquireURI()
|
||||||
|
if err := u.Parse(resp.Header.Peek(http.HeaderHost), []byte(link.URL)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &domain.URL{URI: u}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractEndpointFromBody(resp *http.Response, name string) (*domain.URL, error) {
|
||||||
|
host, err := url.Parse(string(resp.Header.Peek(http.HeaderHost)))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot parse host header: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoints, ok := microformats.Parse(bytes.NewReader(resp.Body()), host).Rels[name]
|
||||||
|
if !ok || len(endpoints) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return domain.NewURL(endpoints[len(endpoints)-1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractFromMetadata(client *http.Client, endpoint *domain.URL) (*domain.URL, error) {
|
||||||
|
_, body, err := client.Get(nil, endpoint.String())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := new(Response)
|
||||||
|
if err = json.Unmarshal(body, resp); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp.TokenEndpoint, nil
|
||||||
|
}
|
|
@ -0,0 +1,104 @@
|
||||||
|
package http_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/fasthttp/router"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
http "github.com/valyala/fasthttp"
|
||||||
|
|
||||||
|
"source.toby3d.me/website/oauth/internal/common"
|
||||||
|
"source.toby3d.me/website/oauth/internal/domain"
|
||||||
|
"source.toby3d.me/website/oauth/internal/testing/httptest"
|
||||||
|
repository "source.toby3d.me/website/oauth/internal/ticket/repository/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TestCase struct {
|
||||||
|
name string
|
||||||
|
bodyLinks map[string]string
|
||||||
|
metadata string
|
||||||
|
}
|
||||||
|
|
||||||
|
const testBody string = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Secret</title>
|
||||||
|
%s
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Nothing to see here.</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`
|
||||||
|
|
||||||
|
func TestGet(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
resource := domain.TestURL(t, "https://alice.example.com/private")
|
||||||
|
endpoint := domain.TestURL(t, "https://example.org/token")
|
||||||
|
|
||||||
|
for _, testCase := range []TestCase{{
|
||||||
|
name: "link",
|
||||||
|
bodyLinks: map[string]string{
|
||||||
|
"token_endpoint": endpoint.String(),
|
||||||
|
},
|
||||||
|
metadata: `{"token_endpoint": ""}`,
|
||||||
|
}, {
|
||||||
|
name: "metadata",
|
||||||
|
bodyLinks: map[string]string{
|
||||||
|
"indieauth-metadata": "https://example.com/.well-known/oauth-authorization-server",
|
||||||
|
},
|
||||||
|
metadata: `{"token_endpoint": "` + endpoint.String() + `"}`,
|
||||||
|
}, {
|
||||||
|
name: "fallback",
|
||||||
|
bodyLinks: map[string]string{
|
||||||
|
"token_endpoint": endpoint.String(),
|
||||||
|
"indieauth-metadata": "https://example.com/.well-known/oauth-authorization-server",
|
||||||
|
},
|
||||||
|
metadata: `{}`,
|
||||||
|
}, {
|
||||||
|
name: "priority",
|
||||||
|
bodyLinks: map[string]string{
|
||||||
|
"token_endpoint": "dont-touch-me",
|
||||||
|
"indieauth-metadata": "https://example.com/.well-known/oauth-authorization-server",
|
||||||
|
},
|
||||||
|
metadata: `{"token_endpoint": "` + endpoint.String() + `"}`,
|
||||||
|
}} {
|
||||||
|
testCase := testCase
|
||||||
|
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
r := router.New()
|
||||||
|
r.GET("/.well-known/oauth-authorization-server", func(ctx *http.RequestCtx) {
|
||||||
|
ctx.SuccessString(common.MIMEApplicationJSONCharsetUTF8, testCase.metadata)
|
||||||
|
})
|
||||||
|
r.GET("/private", func(ctx *http.RequestCtx) {
|
||||||
|
bodyLinks := make([]string, 0)
|
||||||
|
for k, v := range testCase.bodyLinks {
|
||||||
|
bodyLinks = append(bodyLinks, `<link rel="`+k+`" href="`+v+`">`)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.SuccessString(
|
||||||
|
common.MIMETextHTMLCharsetUTF8,
|
||||||
|
fmt.Sprintf(testBody, strings.Join(bodyLinks, "\n")),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
client, _, cleanup := httptest.New(t, r.Handler)
|
||||||
|
t.Cleanup(cleanup)
|
||||||
|
|
||||||
|
result, err := repository.NewHTTPTicketRepository(client).
|
||||||
|
Get(context.Background(), resource)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, endpoint.String(), result.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
package memory
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"path"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"source.toby3d.me/website/oauth/internal/domain"
|
||||||
|
"source.toby3d.me/website/oauth/internal/ticket"
|
||||||
|
)
|
||||||
|
|
||||||
|
type memoryTicketRepository struct {
|
||||||
|
store *sync.Map
|
||||||
|
}
|
||||||
|
|
||||||
|
const DefaultPathPrefix string = "tickets"
|
||||||
|
|
||||||
|
func NewMemoryTicketRepository(store *sync.Map) ticket.Repository {
|
||||||
|
return &memoryTicketRepository{
|
||||||
|
store: store,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *memoryTicketRepository) Get(_ context.Context, resource *domain.URL) (*domain.URL, error) {
|
||||||
|
src, ok := repo.store.Load(path.Join(DefaultPathPrefix, resource.String()))
|
||||||
|
if !ok {
|
||||||
|
return nil, ticket.ErrNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
result, ok := src.(*domain.URL)
|
||||||
|
if !ok {
|
||||||
|
return nil, ticket.ErrNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
package memory_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"path"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"source.toby3d.me/website/oauth/internal/domain"
|
||||||
|
repository "source.toby3d.me/website/oauth/internal/ticket/repository/memory"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGet(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ticket := domain.TestTicket(t)
|
||||||
|
user := domain.TestUser(t)
|
||||||
|
|
||||||
|
store := new(sync.Map)
|
||||||
|
store.Store(path.Join(repository.DefaultPathPrefix, ticket.Resource.String()), user.TokenEndpoint)
|
||||||
|
|
||||||
|
result, err := repository.NewMemoryTicketRepository(store).Get(context.Background(), ticket.Resource)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, user.TokenEndpoint.String(), result.String())
|
||||||
|
}
|
|
@ -8,5 +8,5 @@ import (
|
||||||
|
|
||||||
type UseCase interface {
|
type UseCase interface {
|
||||||
// Redeem transform received ticket into access token.
|
// Redeem transform received ticket into access token.
|
||||||
Redeem(ctx context.Context, endpoint *domain.URL, ticket string) (*domain.Token, error)
|
Redeem(ctx context.Context, ticket *domain.Ticket) (*domain.Token, error)
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,16 +23,23 @@ type (
|
||||||
|
|
||||||
ticketUseCase struct {
|
ticketUseCase struct {
|
||||||
client *http.Client
|
client *http.Client
|
||||||
|
repo ticket.Repository
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewTicketUseCase(client *http.Client) ticket.UseCase {
|
func NewTicketUseCase(repo ticket.Repository, client *http.Client) ticket.UseCase {
|
||||||
return &ticketUseCase{
|
return &ticketUseCase{
|
||||||
client: client,
|
client: client,
|
||||||
|
repo: repo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (useCase *ticketUseCase) Redeem(ctx context.Context, endpoint *domain.URL, ticket string) (*domain.Token, error) {
|
func (useCase *ticketUseCase) Redeem(ctx context.Context, ticket *domain.Ticket) (*domain.Token, error) {
|
||||||
|
endpoint, err := useCase.repo.Get(ctx, ticket.Resource)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot discovery token endpoint: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
req := http.AcquireRequest()
|
req := http.AcquireRequest()
|
||||||
defer http.ReleaseRequest(req)
|
defer http.ReleaseRequest(req)
|
||||||
req.Header.SetMethod(http.MethodPost)
|
req.Header.SetMethod(http.MethodPost)
|
||||||
|
@ -40,7 +47,7 @@ func (useCase *ticketUseCase) Redeem(ctx context.Context, endpoint *domain.URL,
|
||||||
req.Header.SetContentType(common.MIMEApplicationForm)
|
req.Header.SetContentType(common.MIMEApplicationForm)
|
||||||
req.Header.Set(http.HeaderAccept, common.MIMEApplicationJSON)
|
req.Header.Set(http.HeaderAccept, common.MIMEApplicationJSON)
|
||||||
req.PostArgs().Set("grant_type", domain.GrantTypeTicket.String())
|
req.PostArgs().Set("grant_type", domain.GrantTypeTicket.String())
|
||||||
req.PostArgs().Set("ticket", ticket)
|
req.PostArgs().Set("ticket", ticket.Ticket)
|
||||||
|
|
||||||
resp := http.AcquireResponse()
|
resp := http.AcquireResponse()
|
||||||
defer http.ReleaseResponse(resp)
|
defer http.ReleaseResponse(resp)
|
||||||
|
|
|
@ -14,8 +14,8 @@ import (
|
||||||
"source.toby3d.me/website/oauth/internal/common"
|
"source.toby3d.me/website/oauth/internal/common"
|
||||||
"source.toby3d.me/website/oauth/internal/domain"
|
"source.toby3d.me/website/oauth/internal/domain"
|
||||||
"source.toby3d.me/website/oauth/internal/testing/httptest"
|
"source.toby3d.me/website/oauth/internal/testing/httptest"
|
||||||
|
repo "source.toby3d.me/website/oauth/internal/ticket/repository/memory"
|
||||||
ucase "source.toby3d.me/website/oauth/internal/ticket/usecase"
|
ucase "source.toby3d.me/website/oauth/internal/ticket/usecase"
|
||||||
userrepo "source.toby3d.me/website/oauth/internal/user/repository/memory"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRedeem(t *testing.T) {
|
func TestRedeem(t *testing.T) {
|
||||||
|
@ -25,20 +25,23 @@ func TestRedeem(t *testing.T) {
|
||||||
ticket := domain.TestTicket(t)
|
ticket := domain.TestTicket(t)
|
||||||
|
|
||||||
store := new(sync.Map)
|
store := new(sync.Map)
|
||||||
store.Store(path.Join(userrepo.DefaultPathPrefix, ticket.Subject.String()), domain.TestUser(t))
|
store.Store(
|
||||||
|
path.Join(repo.DefaultPathPrefix, ticket.Resource.String()),
|
||||||
|
domain.TestURL(t, "https://example.com/token"),
|
||||||
|
)
|
||||||
|
|
||||||
client, _, cleanup := httptest.New(t, func(ctx *http.RequestCtx) {
|
client, _, cleanup := httptest.New(t, func(ctx *http.RequestCtx) {
|
||||||
ctx.SuccessString(common.MIMEApplicationJSONCharsetUTF8, fmt.Sprintf(`{
|
ctx.SuccessString(common.MIMEApplicationJSONCharsetUTF8, fmt.Sprintf(`{
|
||||||
"access_token": "%s",
|
|
||||||
"token_type": "Bearer",
|
"token_type": "Bearer",
|
||||||
|
"access_token": "%s",
|
||||||
"scope": "%s",
|
"scope": "%s",
|
||||||
"me": "%s"
|
"me": "%s"
|
||||||
}`, token.AccessToken, token.Scope.String(), token.Me.String()))
|
}`, token.AccessToken, token.Scope.String(), token.Me.String()))
|
||||||
})
|
})
|
||||||
t.Cleanup(cleanup)
|
t.Cleanup(cleanup)
|
||||||
|
|
||||||
result, err := ucase.NewTicketUseCase(client).
|
result, err := ucase.NewTicketUseCase(repo.NewMemoryTicketRepository(store), client).
|
||||||
Redeem(context.Background(), domain.TestURL(t, "https://bob.example.com/token"), ticket.Ticket)
|
Redeem(context.Background(), ticket)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, token.AccessToken, result.AccessToken)
|
assert.Equal(t, token.AccessToken, result.AccessToken)
|
||||||
assert.Equal(t, token.Me.String(), result.Me.String())
|
assert.Equal(t, token.Me.String(), result.Me.String())
|
||||||
|
|
Loading…
Reference in New Issue