🔀 Merge branch 'feature/token' into develop
This commit is contained in:
commit
317c84786a
|
@ -0,0 +1,7 @@
|
||||||
|
package common
|
||||||
|
|
||||||
|
const (
|
||||||
|
MIMEApplicationJSON string = "application/json"
|
||||||
|
MIMETextHTML string = "text/html"
|
||||||
|
MIMEApplicationXWWWFormUrlencoded string = "application/x-www-form-urlencoded"
|
||||||
|
)
|
|
@ -6,8 +6,6 @@ import (
|
||||||
"golang.org/x/xerrors"
|
"golang.org/x/xerrors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO(toby3d): make more informative errors.
|
|
||||||
// See https://indieauth.spec.indieweb.org/#authorization-request
|
|
||||||
type Error struct {
|
type Error struct {
|
||||||
Code string `json:"error"`
|
Code string `json:"error"`
|
||||||
Description string `json:"error_description,omitempty"`
|
Description string `json:"error_description,omitempty"`
|
||||||
|
@ -15,48 +13,43 @@ type Error struct {
|
||||||
Frame xerrors.Frame `json:"-"`
|
Frame xerrors.Frame `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
const (
|
||||||
ErrInvalidRequest Error = Error{
|
ErrAccessDenied string = "access_denied"
|
||||||
Code: "invalid_request",
|
ErrInvalidClient string = "invalid_client"
|
||||||
Description: "the request is missing a required parameter, includes an invalid parameter value, or is otherwise malformed",
|
ErrInvalidGrant string = "invalid_grant"
|
||||||
}
|
ErrInvalidRequest string = "invalid_request"
|
||||||
ErrUnauthorizedClient Error = Error{
|
ErrInvalidScope string = "invalid_scope"
|
||||||
Code: "unauthorized_client",
|
ErrInvalidToken string = "invalid_token"
|
||||||
Description: "the client is not authorized to request an authorization code using this method",
|
ErrServerError string = "server_error"
|
||||||
}
|
ErrTemporarilyUnavailable string = "temporarily_unavailable"
|
||||||
ErrAccessDenied Error = Error{
|
ErrUnauthorizedClient string = "unauthorized_client"
|
||||||
Code: "access_denied",
|
ErrUnsupportedResponseType string = "unsupported_response_type"
|
||||||
Description: "",
|
|
||||||
}
|
|
||||||
ErrUnsupportedResponseType Error = Error{
|
|
||||||
Code: "unsupported_response_type",
|
|
||||||
Description: "the authorization server does not support obtaining an authorization code using this method",
|
|
||||||
}
|
|
||||||
ErrInvalidScope Error = Error{
|
|
||||||
Code: "invalid_scope",
|
|
||||||
Description: "the requested scope is invalid, unknown, or malformed",
|
|
||||||
}
|
|
||||||
ErrServerError Error = Error{
|
|
||||||
Code: "server_error",
|
|
||||||
Description: "the authorization server encountered an unexpected condition which prevented it from fulfilling the request",
|
|
||||||
}
|
|
||||||
ErrTemporarilyUnavailable Error = Error{
|
|
||||||
Code: "temporarily_unavailable",
|
|
||||||
Description: "the authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server",
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (e Error) FormatError(p xerrors.Printer) error {
|
const errorColor string = "\033[31m"
|
||||||
p.Printf("%s: %s", e.Code, e.Description)
|
|
||||||
e.Frame.Format(p)
|
|
||||||
|
|
||||||
return nil
|
func (e Error) Error() string {
|
||||||
|
return fmt.Sprint(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e Error) Format(s fmt.State, r rune) {
|
func (e Error) Format(s fmt.State, r rune) {
|
||||||
xerrors.FormatError(e, s, r)
|
xerrors.FormatError(e, s, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e Error) Error() string {
|
func (e Error) FormatError(p xerrors.Printer) error {
|
||||||
return fmt.Sprint(e)
|
p.Print(errorColor, e.Code)
|
||||||
|
|
||||||
|
if e.Description != "" {
|
||||||
|
p.Printf(": %s", e.Description)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.URI != "" {
|
||||||
|
p.Printf("%4s", e.URI)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Detail() {
|
||||||
|
e.Frame.Format(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
package model
|
package model
|
||||||
|
|
||||||
type Profile struct {
|
type Profile struct {
|
||||||
Name string `json:"name"`
|
Name string
|
||||||
URL string `json:"url"`
|
URL URL
|
||||||
Photo string `json:"photo"`
|
Photo URL
|
||||||
Email string `json:"email,omitempty"`
|
Email string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProfile() *Profile {
|
||||||
|
return new(Profile)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,17 @@
|
||||||
package model
|
package model
|
||||||
|
|
||||||
type Token struct {
|
type Token struct {
|
||||||
AccessToken string `json:"access_token"`
|
Profile *Profile
|
||||||
TokenType string `json:"token_type"`
|
Scopes []string
|
||||||
Scope string `json:"scope"`
|
AccessToken string
|
||||||
Me string `json:"me"`
|
Type string
|
||||||
ClientID string `json:"client_id"`
|
Me URL
|
||||||
Profile *Profile `json:"profile,omitempty"`
|
ClientID URL
|
||||||
}
|
}
|
||||||
|
|
||||||
func (Token) Bucket() []byte { return []byte("tokens") }
|
func NewToken() *Token {
|
||||||
|
t := new(Token)
|
||||||
|
t.Scopes = make([]string, 0)
|
||||||
|
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
type URL string
|
|
@ -1,186 +1,151 @@
|
||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/fasthttp/router"
|
"github.com/fasthttp/router"
|
||||||
json "github.com/goccy/go-json"
|
json "github.com/goccy/go-json"
|
||||||
http "github.com/valyala/fasthttp"
|
http "github.com/valyala/fasthttp"
|
||||||
"gitlab.com/toby3d/indieauth/internal/model"
|
"golang.org/x/xerrors"
|
||||||
"gitlab.com/toby3d/indieauth/internal/token"
|
"source.toby3d.me/website/oauth/internal/common"
|
||||||
|
"source.toby3d.me/website/oauth/internal/model"
|
||||||
|
"source.toby3d.me/website/oauth/internal/token"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
Handler struct {
|
RequestHandler struct {
|
||||||
useCase token.UseCase
|
useCase token.UseCase
|
||||||
}
|
}
|
||||||
|
|
||||||
ExchangeRequest struct {
|
RevocationRequest struct {
|
||||||
GrantType string
|
|
||||||
Code string
|
|
||||||
ClientID string
|
|
||||||
RedirectURI string
|
|
||||||
CodeVerifier string
|
|
||||||
}
|
|
||||||
|
|
||||||
RevokeRequest struct {
|
|
||||||
Action string
|
Action string
|
||||||
Token string
|
Token string
|
||||||
}
|
}
|
||||||
|
|
||||||
ExchangeResponse struct {
|
//nolint: tagliatelle
|
||||||
AccessToken string `json:"access_token"`
|
VerificationResponse struct {
|
||||||
Me string `json:"me"`
|
Me model.URL `json:"me"`
|
||||||
Scope string `json:"scope"`
|
ClientID model.URL `json:"client_id"`
|
||||||
TokenType string `json:"token_type"`
|
Scope string `json:"scope"`
|
||||||
}
|
}
|
||||||
|
|
||||||
VerificationResponse struct {
|
RevocationResponse struct{}
|
||||||
Me string `json:"me"`
|
|
||||||
ClientID string `json:"client_id"`
|
|
||||||
Scope string `json:"scope"`
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewTokenHandler(useCase token.UseCase) *Handler {
|
const (
|
||||||
return &Handler{
|
Action string = "action"
|
||||||
|
ActionRevoke string = "revoke"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewRequestHandler(useCase token.UseCase) *RequestHandler {
|
||||||
|
return &RequestHandler{
|
||||||
useCase: useCase,
|
useCase: useCase,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) Register(r *router.Router) {
|
func (h *RequestHandler) Register(r *router.Router) {
|
||||||
r.GET("/token", h.Verification)
|
r.GET("/token", h.Read)
|
||||||
r.POST("/token", h.Update)
|
r.POST("/token", h.Update)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) Verification(ctx *http.RequestCtx) {
|
func (h *RequestHandler) Read(ctx *http.RequestCtx) {
|
||||||
|
ctx.SetContentType(common.MIMEApplicationJSON)
|
||||||
|
ctx.SetStatusCode(http.StatusOK)
|
||||||
|
|
||||||
encoder := json.NewEncoder(ctx)
|
encoder := json.NewEncoder(ctx)
|
||||||
|
rawToken := ctx.Request.Header.Peek(http.HeaderAuthorization)
|
||||||
|
|
||||||
token, err := h.useCase.Verify(ctx,
|
token, err := h.useCase.Verify(ctx, string(bytes.TrimSpace(bytes.TrimPrefix(rawToken, []byte("Bearer")))))
|
||||||
strings.TrimPrefix(string(ctx.Request.Header.Peek(http.HeaderAuthorization)), "Bearer "),
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Error(err.Error(), http.StatusBadRequest)
|
ctx.SetStatusCode(http.StatusBadRequest)
|
||||||
|
|
||||||
return
|
if err = encoder.Encode(err); err != nil {
|
||||||
}
|
ctx.Error(err.Error(), http.StatusInternalServerError)
|
||||||
|
|
||||||
ctx.SetContentType("application/json")
|
|
||||||
_ = encoder.Encode(&VerificationResponse{
|
|
||||||
Me: token.Me,
|
|
||||||
ClientID: token.ClientID,
|
|
||||||
Scope: token.Scope,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) Update(ctx *http.RequestCtx) {
|
|
||||||
if ctx.PostArgs().Has("action") {
|
|
||||||
h.Revoke(ctx)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
h.Exchange(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ExchangeRequest) bind(ctx *http.RequestCtx) error {
|
|
||||||
if r.GrantType = string(ctx.PostArgs().Peek("grant_type")); r.GrantType != "authorization_code" {
|
|
||||||
return model.Error{
|
|
||||||
Code: model.ErrInvalidRequest.Code,
|
|
||||||
Description: "'grant_type' must be 'authorization_code'",
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if r.Code = string(ctx.PostArgs().Peek("code")); r.Code == "" {
|
|
||||||
return model.Error{
|
|
||||||
Code: model.ErrInvalidRequest.Code,
|
|
||||||
Description: "'code' query is required",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.ClientID = string(ctx.PostArgs().Peek("client_id")); r.ClientID == "" {
|
|
||||||
return model.Error{
|
|
||||||
Code: model.ErrInvalidRequest.Code,
|
|
||||||
Description: "'client_id' query is required",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.RedirectURI = string(ctx.PostArgs().Peek("redirect_uri")); r.RedirectURI == "" {
|
|
||||||
return model.Error{
|
|
||||||
Code: model.ErrInvalidRequest.Code,
|
|
||||||
Description: "'redirect_uri' query is required",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
r.CodeVerifier = string(ctx.PostArgs().Peek("code_verifier"))
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) Exchange(ctx *http.RequestCtx) {
|
|
||||||
encoder := json.NewEncoder(ctx)
|
|
||||||
req := new(ExchangeRequest)
|
|
||||||
|
|
||||||
if err := req.bind(ctx); err != nil {
|
|
||||||
ctx.Error(err.Error(), http.StatusBadRequest)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
token, err := h.useCase.Exchange(ctx, &model.ExchangeRequest{
|
|
||||||
ClientID: req.ClientID,
|
|
||||||
Code: req.Code,
|
|
||||||
CodeVerifier: req.CodeVerifier,
|
|
||||||
RedirectURI: req.RedirectURI,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
ctx.Error(err.Error(), http.StatusInternalServerError)
|
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if token == nil {
|
if token == nil {
|
||||||
ctx.Error(model.ErrUnauthorizedClient.Error(), http.StatusUnauthorized)
|
ctx.SetStatusCode(http.StatusUnauthorized)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.SetContentType("application/json")
|
if err := encoder.Encode(&VerificationResponse{
|
||||||
_ = encoder.Encode(&ExchangeResponse{
|
Me: token.Me,
|
||||||
AccessToken: token.AccessToken,
|
ClientID: token.ClientID,
|
||||||
Me: token.Me,
|
Scope: strings.Join(token.Scopes, " "),
|
||||||
Scope: token.Scope,
|
}); err != nil {
|
||||||
TokenType: token.TokenType,
|
ctx.SetStatusCode(http.StatusInternalServerError)
|
||||||
})
|
|
||||||
|
if err = encoder.Encode(err); err != nil {
|
||||||
|
ctx.Error(err.Error(), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RevokeRequest) bind(ctx *http.RequestCtx) error {
|
func (h *RequestHandler) Update(ctx *http.RequestCtx) {
|
||||||
if r.Action = string(ctx.PostArgs().Peek("action")); r.Action != "revoke" {
|
if strings.EqualFold(string(ctx.FormValue(Action)), ActionRevoke) {
|
||||||
|
h.Revocation(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *RequestHandler) Revocation(ctx *http.RequestCtx) {
|
||||||
|
ctx.SetContentType(common.MIMEApplicationJSON)
|
||||||
|
ctx.SetStatusCode(http.StatusOK)
|
||||||
|
|
||||||
|
encoder := json.NewEncoder(ctx)
|
||||||
|
|
||||||
|
req := new(RevocationRequest)
|
||||||
|
if err := req.bind(ctx); err != nil {
|
||||||
|
ctx.SetStatusCode(http.StatusBadRequest)
|
||||||
|
|
||||||
|
if err = encoder.Encode(err); err != nil {
|
||||||
|
ctx.Error(err.Error(), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.useCase.Revoke(ctx, req.Token); err != nil {
|
||||||
|
ctx.SetStatusCode(http.StatusBadRequest)
|
||||||
|
|
||||||
|
if err = encoder.Encode(err); err != nil {
|
||||||
|
ctx.Error(err.Error(), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := encoder.Encode(&RevocationResponse{}); err != nil {
|
||||||
|
ctx.SetStatusCode(http.StatusInternalServerError)
|
||||||
|
|
||||||
|
if err = encoder.Encode(err); err != nil {
|
||||||
|
ctx.Error(err.Error(), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RevocationRequest) bind(ctx *http.RequestCtx) error {
|
||||||
|
if r.Action = string(ctx.FormValue(Action)); !strings.EqualFold(r.Action, ActionRevoke) {
|
||||||
return model.Error{
|
return model.Error{
|
||||||
Code: model.ErrInvalidRequest.Code,
|
Code: "invalid_request",
|
||||||
Description: "'action' must be 'revoke'",
|
Description: "request MUST contain 'action' key with value 'revoke'",
|
||||||
|
URI: "https://indieauth.spec.indieweb.org/#token-revocation-request",
|
||||||
|
Frame: xerrors.Caller(1),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.Token = string(ctx.PostArgs().Peek("token")); r.Token == "" {
|
if r.Token = string(ctx.FormValue("token")); r.Token == "" {
|
||||||
return model.Error{
|
return model.Error{
|
||||||
Code: model.ErrInvalidRequest.Code,
|
Code: "invalid_request",
|
||||||
Description: "'token' query is required",
|
Description: "request MUST contain the 'token' key with the valid access token as its value",
|
||||||
|
URI: "https://indieauth.spec.indieweb.org/#token-revocation-request",
|
||||||
|
Frame: xerrors.Caller(1),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) Revoke(ctx *http.RequestCtx) {
|
|
||||||
req := new(RevokeRequest)
|
|
||||||
if err := req.bind(ctx); err != nil {
|
|
||||||
ctx.Error(err.Error(), http.StatusBadRequest)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = h.useCase.Revoke(ctx, string(ctx.PostArgs().Peek("token")))
|
|
||||||
|
|
||||||
ctx.SuccessString("application/json", "{}")
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,101 @@
|
||||||
|
package http_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/brianvoe/gofakeit"
|
||||||
|
"github.com/goccy/go-json"
|
||||||
|
"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/model"
|
||||||
|
delivery "source.toby3d.me/website/oauth/internal/token/delivery/http"
|
||||||
|
repository "source.toby3d.me/website/oauth/internal/token/repository/memory"
|
||||||
|
"source.toby3d.me/website/oauth/internal/token/usecase"
|
||||||
|
"source.toby3d.me/website/oauth/internal/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestVerification(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
require := require.New(t)
|
||||||
|
assert := assert.New(t)
|
||||||
|
store := new(sync.Map)
|
||||||
|
repo := repository.NewMemoryTokenRepository(store)
|
||||||
|
accessToken := model.Token{
|
||||||
|
AccessToken: gofakeit.Password(true, true, true, true, false, 32),
|
||||||
|
Type: "Bearer",
|
||||||
|
ClientID: "https://app.example.com/",
|
||||||
|
Me: "https://user.example.net/",
|
||||||
|
Scopes: []string{"create", "update", "delete"},
|
||||||
|
Profile: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(repo.Create(context.TODO(), &accessToken))
|
||||||
|
|
||||||
|
req := http.AcquireRequest()
|
||||||
|
defer http.ReleaseRequest(req)
|
||||||
|
|
||||||
|
req.Header.SetMethod(http.MethodGet)
|
||||||
|
req.SetRequestURI("http://localhost/token")
|
||||||
|
req.Header.Set(http.HeaderAccept, common.MIMEApplicationJSON)
|
||||||
|
req.Header.Set(http.HeaderAuthorization, "Bearer "+accessToken.AccessToken)
|
||||||
|
|
||||||
|
resp := http.AcquireResponse()
|
||||||
|
defer http.ReleaseResponse(resp)
|
||||||
|
|
||||||
|
require.NoError(util.Serve(delivery.NewRequestHandler(usecase.NewTokenUseCase(repo)).Read, req, resp))
|
||||||
|
assert.Equal(http.StatusOK, resp.StatusCode())
|
||||||
|
|
||||||
|
token := new(delivery.VerificationResponse)
|
||||||
|
require.NoError(json.Unmarshal(resp.Body(), token))
|
||||||
|
assert.Equal(&delivery.VerificationResponse{
|
||||||
|
Me: accessToken.Me,
|
||||||
|
ClientID: accessToken.ClientID,
|
||||||
|
Scope: strings.Join(accessToken.Scopes, " "),
|
||||||
|
}, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRevocation(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
require := require.New(t)
|
||||||
|
assert := assert.New(t)
|
||||||
|
store := new(sync.Map)
|
||||||
|
repo := repository.NewMemoryTokenRepository(store)
|
||||||
|
accessToken := gofakeit.Password(true, true, true, true, false, 32)
|
||||||
|
|
||||||
|
require.NoError(repo.Create(context.TODO(), &model.Token{
|
||||||
|
AccessToken: accessToken,
|
||||||
|
Type: "Bearer",
|
||||||
|
ClientID: "https://app.example.com/",
|
||||||
|
Me: "https://user.example.net/",
|
||||||
|
Scopes: []string{"create", "update", "delete"},
|
||||||
|
Profile: nil,
|
||||||
|
}))
|
||||||
|
|
||||||
|
req := http.AcquireRequest()
|
||||||
|
defer http.ReleaseRequest(req)
|
||||||
|
|
||||||
|
req.Header.SetMethod(http.MethodPost)
|
||||||
|
req.SetRequestURI("http://localhost/token")
|
||||||
|
req.Header.SetContentType(common.MIMEApplicationXWWWFormUrlencoded)
|
||||||
|
req.Header.Set(http.HeaderAccept, common.MIMEApplicationJSON)
|
||||||
|
req.PostArgs().Set("action", "revoke")
|
||||||
|
req.PostArgs().Set("token", accessToken)
|
||||||
|
|
||||||
|
resp := http.AcquireResponse()
|
||||||
|
defer http.ReleaseResponse(resp)
|
||||||
|
|
||||||
|
require.NoError(util.Serve(delivery.NewRequestHandler(usecase.NewTokenUseCase(repo)).Update, req, resp))
|
||||||
|
assert.Equal(http.StatusOK, resp.StatusCode())
|
||||||
|
assert.Equal(`{}`, strings.TrimSpace(string(resp.Body())))
|
||||||
|
|
||||||
|
token, err := repo.Get(context.TODO(), accessToken)
|
||||||
|
require.NoError(err)
|
||||||
|
assert.Nil(token)
|
||||||
|
}
|
|
@ -3,11 +3,20 @@ package token
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"gitlab.com/toby3d/indieauth/internal/model"
|
"golang.org/x/xerrors"
|
||||||
|
"source.toby3d.me/website/oauth/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Repository interface {
|
type Repository interface {
|
||||||
Create(ctx context.Context, token *model.Token) error
|
Get(ctx context.Context, accessToken string) (*model.Token, error)
|
||||||
Get(ctx context.Context, token string) (*model.Token, error)
|
Create(ctx context.Context, accessToken *model.Token) error
|
||||||
Delete(ctx context.Context, token string) error
|
Update(ctx context.Context, accessToken *model.Token) error
|
||||||
|
Remove(ctx context.Context, accessToken string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrExist error = model.Error{
|
||||||
|
Code: "invalid_request",
|
||||||
|
Description: "this token is already exists",
|
||||||
|
URI: "",
|
||||||
|
Frame: xerrors.Caller(1),
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,24 +2,40 @@ package bolt
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
json "github.com/goccy/go-json"
|
json "github.com/goccy/go-json"
|
||||||
"gitlab.com/toby3d/indieauth/internal/model"
|
"github.com/pkg/errors"
|
||||||
"gitlab.com/toby3d/indieauth/internal/token"
|
|
||||||
bolt "go.etcd.io/bbolt"
|
bolt "go.etcd.io/bbolt"
|
||||||
|
"golang.org/x/xerrors"
|
||||||
|
"source.toby3d.me/website/oauth/internal/model"
|
||||||
|
"source.toby3d.me/website/oauth/internal/token"
|
||||||
)
|
)
|
||||||
|
|
||||||
type boltTokenRepository struct {
|
type (
|
||||||
db *bolt.DB
|
Token struct {
|
||||||
}
|
AccessToken string `json:"accessToken"`
|
||||||
|
ClientID string `json:"clientId"`
|
||||||
|
Me string `json:"me"`
|
||||||
|
Scope string `json:"scope"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
boltTokenRepository struct {
|
||||||
|
db *bolt.DB
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrNotExist error = xerrors.New("key not exist")
|
||||||
|
|
||||||
func NewBoltTokenRepository(db *bolt.DB) (token.Repository, error) {
|
func NewBoltTokenRepository(db *bolt.DB) (token.Repository, error) {
|
||||||
if err := db.Update(func(tx *bolt.Tx) error {
|
if err := db.Update(func(tx *bolt.Tx) error {
|
||||||
_, err := tx.CreateBucketIfNotExists(model.Token{}.Bucket())
|
//nolint: exhaustivestruct
|
||||||
|
_, err := tx.CreateBucketIfNotExists(Token{}.Bucket())
|
||||||
|
|
||||||
return err
|
return errors.Wrap(err, "failed to create a bucket")
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return nil, err
|
return nil, errors.Wrap(err, "failed to update the storage structure")
|
||||||
}
|
}
|
||||||
|
|
||||||
return &boltTokenRepository{
|
return &boltTokenRepository{
|
||||||
|
@ -27,31 +43,101 @@ func NewBoltTokenRepository(db *bolt.DB) (token.Repository, error) {
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (repo *boltTokenRepository) Create(ctx context.Context, token *model.Token) error {
|
func (repo *boltTokenRepository) Get(ctx context.Context, accessToken string) (*model.Token, error) {
|
||||||
jsonToken, err := json.Marshal(token)
|
result := model.NewToken()
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return repo.db.Update(func(tx *bolt.Tx) error {
|
|
||||||
return tx.Bucket(model.Token{}.Bucket()).Put([]byte(token.AccessToken), jsonToken)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *boltTokenRepository) Get(ctx context.Context, token string) (*model.Token, error) {
|
|
||||||
t := new(model.Token)
|
|
||||||
|
|
||||||
if err := repo.db.View(func(tx *bolt.Tx) error {
|
if err := repo.db.View(func(tx *bolt.Tx) error {
|
||||||
return json.Unmarshal(tx.Bucket(model.Token{}.Bucket()).Get([]byte(token)), t)
|
//nolint: exhaustivestruct
|
||||||
|
if src := tx.Bucket(Token{}.Bucket()).Get([]byte(accessToken)); src != nil {
|
||||||
|
return NewToken().Bind(src, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ErrNotExist
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return nil, err
|
if !xerrors.Is(err, ErrNotExist) {
|
||||||
|
return nil, errors.Wrap(err, "failed to retrieve token from storage")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return t, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (repo *boltTokenRepository) Delete(ctx context.Context, token string) error {
|
func (repo *boltTokenRepository) Create(ctx context.Context, accessToken *model.Token) error {
|
||||||
return repo.db.Update(func(tx *bolt.Tx) error {
|
t, err := repo.Get(ctx, accessToken.AccessToken)
|
||||||
return tx.Bucket(model.Token{}.Bucket()).Delete([]byte(token))
|
if err != nil {
|
||||||
})
|
return errors.Wrap(err, "failed to verify the existence of the token")
|
||||||
|
}
|
||||||
|
|
||||||
|
if t != nil {
|
||||||
|
return token.ErrExist
|
||||||
|
}
|
||||||
|
|
||||||
|
return repo.Update(ctx, accessToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *boltTokenRepository) Update(ctx context.Context, accessToken *model.Token) error {
|
||||||
|
dto := NewToken()
|
||||||
|
dto.Populate(accessToken)
|
||||||
|
|
||||||
|
src, err := json.Marshal(dto)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to marshal token")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = repo.db.Update(func(tx *bolt.Tx) error {
|
||||||
|
if err := tx.Bucket(dto.Bucket()).Put([]byte(dto.AccessToken), src); err != nil {
|
||||||
|
return errors.Wrap(err, "failed to overwrite the token in the bucket")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return errors.Wrap(err, "failed to update the token in the repository")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *boltTokenRepository) Remove(ctx context.Context, accessToken string) error {
|
||||||
|
if err := repo.db.Update(func(tx *bolt.Tx) error {
|
||||||
|
//nolint: exhaustivestruct
|
||||||
|
if err := tx.Bucket(Token{}.Bucket()).Delete([]byte(accessToken)); err != nil {
|
||||||
|
return errors.Wrap(err, "failed to remove token in bucket")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return errors.Wrap(err, "failed to remove token from storage")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewToken() *Token {
|
||||||
|
return new(Token)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Token) Bucket() []byte { return []byte("tokens") }
|
||||||
|
|
||||||
|
func (t *Token) Populate(src *model.Token) {
|
||||||
|
t.AccessToken = src.AccessToken
|
||||||
|
t.ClientID = string(src.ClientID)
|
||||||
|
t.Me = string(src.Me)
|
||||||
|
t.Scope = strings.Join(src.Scopes, " ")
|
||||||
|
t.Type = src.Type
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Token) Bind(src []byte, dst *model.Token) error {
|
||||||
|
if err := json.Unmarshal(src, t); err != nil {
|
||||||
|
return errors.Wrap(err, "cannot unmarshal token source")
|
||||||
|
}
|
||||||
|
|
||||||
|
dst.AccessToken = t.AccessToken
|
||||||
|
dst.Scopes = strings.Fields(t.Scope)
|
||||||
|
dst.Type = t.Type
|
||||||
|
dst.ClientID = model.URL(t.ClientID)
|
||||||
|
dst.Me = model.URL(t.Me)
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,205 @@
|
||||||
|
//nolint: wrapcheck
|
||||||
|
package bolt_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
json "github.com/goccy/go-json"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.etcd.io/bbolt"
|
||||||
|
"source.toby3d.me/website/oauth/internal/model"
|
||||||
|
"source.toby3d.me/website/oauth/internal/random"
|
||||||
|
"source.toby3d.me/website/oauth/internal/token"
|
||||||
|
"source.toby3d.me/website/oauth/internal/token/repository/bolt"
|
||||||
|
)
|
||||||
|
|
||||||
|
//nolint: gochecknoglobals
|
||||||
|
var (
|
||||||
|
db *bbolt.DB
|
||||||
|
repo token.Repository
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
dbPath := filepath.Join("..", "..", "..", "..", "test", "testing.db")
|
||||||
|
if db, err = bbolt.Open(dbPath, os.ModePerm, nil); err != nil {
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if repo, err = bolt.NewBoltTokenRepository(db); err != nil {
|
||||||
|
_ = db.Close()
|
||||||
|
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
code := m.Run()
|
||||||
|
_ = db.Close()
|
||||||
|
_ = os.RemoveAll(dbPath)
|
||||||
|
|
||||||
|
os.Exit(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGet(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
accessToken := random.New().String(32)
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
_ = db.Update(func(tx *bbolt.Tx) error {
|
||||||
|
//nolint: exhaustivestruct
|
||||||
|
return tx.Bucket(bolt.Token{}.Bucket()).Delete([]byte(accessToken))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
src, err := json.Marshal(&bolt.Token{
|
||||||
|
AccessToken: accessToken,
|
||||||
|
ClientID: "https://app.example.com/",
|
||||||
|
Me: "https://toby3d.me/",
|
||||||
|
Scope: "read update delete",
|
||||||
|
Type: "Bearer",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.NoError(t, db.Update(func(tx *bbolt.Tx) error {
|
||||||
|
//nolint: exhaustivestruct
|
||||||
|
return tx.Bucket(bolt.Token{}.Bucket()).Put([]byte(accessToken), src)
|
||||||
|
}))
|
||||||
|
|
||||||
|
tkn, err := repo.Get(context.TODO(), accessToken)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, &model.Token{
|
||||||
|
AccessToken: accessToken,
|
||||||
|
ClientID: "https://app.example.com/",
|
||||||
|
Me: "https://toby3d.me/",
|
||||||
|
Scopes: []string{"read", "update", "delete"},
|
||||||
|
Type: "Bearer",
|
||||||
|
Profile: nil,
|
||||||
|
}, tkn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreate(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
accessToken := random.New().String(32)
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
_ = db.Update(func(tx *bbolt.Tx) error {
|
||||||
|
//nolint: exhaustivestruct
|
||||||
|
return tx.Bucket(bolt.Token{}.Bucket()).Delete([]byte(accessToken))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
tkn := &model.Token{
|
||||||
|
AccessToken: accessToken,
|
||||||
|
ClientID: "https://app.example.com/",
|
||||||
|
Me: "https://toby3d.me/",
|
||||||
|
Scopes: []string{"read", "update", "delete"},
|
||||||
|
Type: "Bearer",
|
||||||
|
Profile: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, repo.Create(context.TODO(), tkn))
|
||||||
|
|
||||||
|
result := model.NewToken()
|
||||||
|
|
||||||
|
require.NoError(t, db.View(func(tx *bbolt.Tx) error {
|
||||||
|
//nolint: exhaustivestruct
|
||||||
|
return bolt.NewToken().Bind(tx.Bucket(bolt.Token{}.Bucket()).Get([]byte(tkn.AccessToken)), result)
|
||||||
|
}))
|
||||||
|
assert.Equal(t, tkn, result)
|
||||||
|
|
||||||
|
assert.EqualError(t, repo.Create(context.TODO(), tkn), token.ErrExist.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdate(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
accessToken := random.New().String(32)
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
_ = db.Update(func(tx *bbolt.Tx) error {
|
||||||
|
//nolint: exhaustivestruct
|
||||||
|
return tx.Bucket(bolt.Token{}.Bucket()).Delete([]byte(accessToken))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
src, err := json.Marshal(&bolt.Token{
|
||||||
|
AccessToken: accessToken,
|
||||||
|
ClientID: "https://app.example.com/",
|
||||||
|
Me: "https://toby3d.me/",
|
||||||
|
Scope: "read update delete",
|
||||||
|
Type: "Bearer",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.NoError(t, db.Update(func(tx *bbolt.Tx) error {
|
||||||
|
//nolint: exhaustivestruct
|
||||||
|
return tx.Bucket(bolt.Token{}.Bucket()).Put([]byte(accessToken), src)
|
||||||
|
}))
|
||||||
|
|
||||||
|
require.NoError(t, repo.Update(context.TODO(), &model.Token{
|
||||||
|
AccessToken: accessToken,
|
||||||
|
ClientID: "https://client.example.com/",
|
||||||
|
Me: "https://toby3d.ru/",
|
||||||
|
Scopes: []string{"read"},
|
||||||
|
Type: "Bearer",
|
||||||
|
Profile: nil,
|
||||||
|
}))
|
||||||
|
|
||||||
|
result := model.NewToken()
|
||||||
|
|
||||||
|
require.NoError(t, db.View(func(tx *bbolt.Tx) error {
|
||||||
|
//nolint: exhaustivestruct
|
||||||
|
return bolt.NewToken().Bind(tx.Bucket(bolt.Token{}.Bucket()).Get([]byte(accessToken)), result)
|
||||||
|
}))
|
||||||
|
assert.Equal(t, &model.Token{
|
||||||
|
AccessToken: accessToken,
|
||||||
|
ClientID: "https://client.example.com/",
|
||||||
|
Me: "https://toby3d.ru/",
|
||||||
|
Scopes: []string{"read"},
|
||||||
|
Type: "Bearer",
|
||||||
|
Profile: nil,
|
||||||
|
}, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDelete(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
accessToken := random.New().String(32)
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
_ = db.Update(func(tx *bbolt.Tx) error {
|
||||||
|
//nolint: exhaustivestruct
|
||||||
|
return tx.Bucket(bolt.Token{}.Bucket()).Delete([]byte(accessToken))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
src, err := json.Marshal(&bolt.Token{
|
||||||
|
AccessToken: accessToken,
|
||||||
|
ClientID: "https://app.example.com/",
|
||||||
|
Me: "https://toby3d.me/",
|
||||||
|
Scope: "read update delete",
|
||||||
|
Type: "Bearer",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.NoError(t, db.Update(func(tx *bbolt.Tx) error {
|
||||||
|
//nolint: exhaustivestruct
|
||||||
|
return tx.Bucket(bolt.Token{}.Bucket()).Put([]byte(accessToken), src)
|
||||||
|
}))
|
||||||
|
|
||||||
|
require.NoError(t, repo.Remove(context.TODO(), accessToken))
|
||||||
|
|
||||||
|
require.NoError(t, db.View(func(tx *bbolt.Tx) error {
|
||||||
|
//nolint: exhaustivestruct
|
||||||
|
assert.Nil(t, tx.Bucket(bolt.Token{}.Bucket()).Get([]byte(accessToken)))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}))
|
||||||
|
}
|
|
@ -4,37 +4,55 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"gitlab.com/toby3d/indieauth/internal/model"
|
"source.toby3d.me/website/oauth/internal/model"
|
||||||
"gitlab.com/toby3d/indieauth/internal/token"
|
"source.toby3d.me/website/oauth/internal/token"
|
||||||
)
|
)
|
||||||
|
|
||||||
type memoryTokenRepository struct {
|
type memoryTokenRepository struct {
|
||||||
tokens *sync.Map
|
tokens *sync.Map
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMemoryTokenRepository() token.Repository {
|
func NewMemoryTokenRepository(tokens *sync.Map) token.Repository {
|
||||||
return &memoryTokenRepository{
|
return &memoryTokenRepository{
|
||||||
tokens: new(sync.Map),
|
tokens: tokens,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (repo *memoryTokenRepository) Create(ctx context.Context, token *model.Token) error {
|
func (repo *memoryTokenRepository) Get(ctx context.Context, accessToken string) (*model.Token, error) {
|
||||||
repo.tokens.Store(token.AccessToken, token)
|
src, ok := repo.tokens.Load(accessToken)
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *memoryTokenRepository) Delete(ctx context.Context, token string) error {
|
|
||||||
repo.tokens.Delete(token)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *memoryTokenRepository) Get(ctx context.Context, token string) (*model.Token, error) {
|
|
||||||
t, ok := repo.tokens.Load(token)
|
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return t.(*model.Token), nil
|
result, ok := src.(*model.Token)
|
||||||
|
if !ok {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *memoryTokenRepository) Create(ctx context.Context, accessToken *model.Token) error {
|
||||||
|
t, err := repo.Get(ctx, accessToken.AccessToken)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if t != nil {
|
||||||
|
return token.ErrExist
|
||||||
|
}
|
||||||
|
|
||||||
|
return repo.Update(ctx, accessToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *memoryTokenRepository) Update(ctx context.Context, accessToken *model.Token) error {
|
||||||
|
repo.tokens.Store(accessToken.AccessToken, accessToken)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *memoryTokenRepository) Remove(ctx context.Context, accessToken string) error {
|
||||||
|
repo.tokens.Delete(accessToken)
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,126 @@
|
||||||
|
package memory_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"source.toby3d.me/website/oauth/internal/model"
|
||||||
|
"source.toby3d.me/website/oauth/internal/random"
|
||||||
|
"source.toby3d.me/website/oauth/internal/token"
|
||||||
|
"source.toby3d.me/website/oauth/internal/token/repository/memory"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGet(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
store := new(sync.Map)
|
||||||
|
accessToken := &model.Token{
|
||||||
|
AccessToken: random.New().String(32),
|
||||||
|
ClientID: "https://app.example.com/",
|
||||||
|
Me: "https://toby3d.me/",
|
||||||
|
Profile: &model.Profile{
|
||||||
|
Name: "Maxim Lebedev",
|
||||||
|
URL: "https://toby3d.me/",
|
||||||
|
Photo: "https://toby3d.me/photo.jpg",
|
||||||
|
Email: "hey@toby3d.me",
|
||||||
|
},
|
||||||
|
Scopes: []string{"read", "update", "delete"},
|
||||||
|
Type: "Bearer",
|
||||||
|
}
|
||||||
|
|
||||||
|
store.Store(accessToken.AccessToken, accessToken)
|
||||||
|
|
||||||
|
result, err := memory.NewMemoryTokenRepository(store).Get(context.TODO(), accessToken.AccessToken)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, accessToken, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreate(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
store := new(sync.Map)
|
||||||
|
accessToken := &model.Token{
|
||||||
|
AccessToken: random.New().String(32),
|
||||||
|
ClientID: "https://app.example.com/",
|
||||||
|
Me: "https://toby3d.me/",
|
||||||
|
Profile: &model.Profile{
|
||||||
|
Name: "Maxim Lebedev",
|
||||||
|
URL: "https://toby3d.me/",
|
||||||
|
Photo: "https://toby3d.me/photo.jpg",
|
||||||
|
Email: "hey@toby3d.me",
|
||||||
|
},
|
||||||
|
Scopes: []string{"read", "update", "delete"},
|
||||||
|
Type: "Bearer",
|
||||||
|
}
|
||||||
|
|
||||||
|
repo := memory.NewMemoryTokenRepository(store)
|
||||||
|
require.NoError(t, repo.Create(context.TODO(), accessToken))
|
||||||
|
|
||||||
|
result, ok := store.Load(accessToken.AccessToken)
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, accessToken, result)
|
||||||
|
|
||||||
|
assert.EqualError(t, repo.Create(context.TODO(), accessToken), token.ErrExist.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdate(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
store := new(sync.Map)
|
||||||
|
accessToken := &model.Token{
|
||||||
|
AccessToken: random.New().String(32),
|
||||||
|
ClientID: "https://app.example.com/",
|
||||||
|
Me: "https://toby3d.me/",
|
||||||
|
Profile: &model.Profile{
|
||||||
|
Name: "Maxim Lebedev",
|
||||||
|
URL: "https://toby3d.me/",
|
||||||
|
Photo: "https://toby3d.me/photo.jpg",
|
||||||
|
Email: "hey@toby3d.me",
|
||||||
|
},
|
||||||
|
Scopes: []string{"read", "update", "delete"},
|
||||||
|
Type: "Bearer",
|
||||||
|
}
|
||||||
|
|
||||||
|
store.Store(accessToken.AccessToken, accessToken)
|
||||||
|
|
||||||
|
tokenCopy := *accessToken
|
||||||
|
tokenCopy.ClientID = "https://client.example.com/"
|
||||||
|
tokenCopy.Me = "https://toby3d.ru/"
|
||||||
|
|
||||||
|
require.NoError(t, memory.NewMemoryTokenRepository(store).Update(context.TODO(), &tokenCopy))
|
||||||
|
|
||||||
|
result, ok := store.Load(accessToken.AccessToken)
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.NotEqual(t, accessToken, result)
|
||||||
|
assert.Equal(t, &tokenCopy, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDelete(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
store := new(sync.Map)
|
||||||
|
accessToken := &model.Token{
|
||||||
|
AccessToken: random.New().String(32),
|
||||||
|
ClientID: "https://app.example.com/",
|
||||||
|
Me: "https://toby3d.me/",
|
||||||
|
Profile: &model.Profile{
|
||||||
|
Name: "Maxim Lebedev",
|
||||||
|
URL: "https://toby3d.me/",
|
||||||
|
Photo: "https://toby3d.me/photo.jpg",
|
||||||
|
Email: "hey@toby3d.me",
|
||||||
|
},
|
||||||
|
Scopes: []string{"read", "update", "delete"},
|
||||||
|
Type: "Bearer",
|
||||||
|
}
|
||||||
|
|
||||||
|
store.Store(accessToken.AccessToken, accessToken)
|
||||||
|
|
||||||
|
require.NoError(t, memory.NewMemoryTokenRepository(store).Remove(context.TODO(), accessToken.AccessToken))
|
||||||
|
|
||||||
|
result, ok := store.Load(accessToken.AccessToken)
|
||||||
|
assert.False(t, ok)
|
||||||
|
assert.Nil(t, result)
|
||||||
|
}
|
|
@ -3,11 +3,10 @@ package token
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"gitlab.com/toby3d/indieauth/internal/model"
|
"source.toby3d.me/website/oauth/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UseCase interface {
|
type UseCase interface {
|
||||||
Exchange(ctx context.Context, req *model.ExchangeRequest) (*model.Token, error)
|
Verify(ctx context.Context, accessToken string) (*model.Token, error)
|
||||||
Verify(ctx context.Context, token string) (*model.Token, error)
|
Revoke(ctx context.Context, accessToken string) error
|
||||||
Revoke(ctx context.Context, token string) error
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,93 +2,35 @@ package usecase
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"time"
|
|
||||||
|
|
||||||
"gitlab.com/toby3d/indieauth/internal/auth"
|
"github.com/pkg/errors"
|
||||||
"gitlab.com/toby3d/indieauth/internal/model"
|
"source.toby3d.me/website/oauth/internal/model"
|
||||||
"gitlab.com/toby3d/indieauth/internal/pkce"
|
"source.toby3d.me/website/oauth/internal/token"
|
||||||
"gitlab.com/toby3d/indieauth/internal/random"
|
|
||||||
"gitlab.com/toby3d/indieauth/internal/token"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type tokenUseCase struct {
|
type tokenUseCase struct {
|
||||||
authRepo auth.Repository
|
tokens token.Repository
|
||||||
tokenRepo token.Repository
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTokenUseCase(authRepo auth.Repository, tokenRepo token.Repository) token.UseCase {
|
func NewTokenUseCase(tokens token.Repository) token.UseCase {
|
||||||
return &tokenUseCase{
|
return &tokenUseCase{
|
||||||
authRepo: authRepo,
|
tokens: tokens,
|
||||||
tokenRepo: tokenRepo,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (useCase *tokenUseCase) Exchange(ctx context.Context, req *model.ExchangeRequest) (*model.Token, error) {
|
func (useCase *tokenUseCase) Verify(ctx context.Context, accessToken string) (*model.Token, error) {
|
||||||
login, err := useCase.authRepo.Get(ctx, req.Code)
|
t, err := useCase.tokens.Get(ctx, accessToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, errors.Wrap(err, "failed to retrieve token from storage")
|
||||||
}
|
}
|
||||||
|
|
||||||
if login == nil {
|
return t, nil
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = useCase.authRepo.Delete(ctx, login.Code)
|
|
||||||
|
|
||||||
if time.Now().UTC().After(time.Unix(login.CreatedAt, 0).Add(10 * time.Minute)) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if login.ClientID != req.ClientID {
|
|
||||||
return nil, model.Error{
|
|
||||||
Code: model.ErrInvalidRequest.Code,
|
|
||||||
Description: "'client_id' value must be identical to the value at the start of the authorization flow",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if login.RedirectURI != req.RedirectURI {
|
|
||||||
return nil, model.Error{
|
|
||||||
Code: model.ErrInvalidRequest.Code,
|
|
||||||
Description: "'client_id' value must be identical to the value at the start of the authorization flow",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if login.CodeChallenge != "" {
|
|
||||||
code, err := pkce.New(login.CodeChallengeMethod)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
code.Verifier = req.CodeVerifier
|
|
||||||
|
|
||||||
code.Generate()
|
|
||||||
|
|
||||||
if login.CodeChallenge != code.Challenge {
|
|
||||||
return nil, model.Error{
|
|
||||||
Code: model.ErrInvalidRequest.Code,
|
|
||||||
Description: "PKCE validation failed, please go through the authorization flow again",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
token := new(model.Token)
|
|
||||||
token.AccessToken = random.New().String(64)
|
|
||||||
token.ClientID = login.ClientID
|
|
||||||
token.Me = login.Me
|
|
||||||
token.Scope = login.Scope
|
|
||||||
token.TokenType = "Bearer"
|
|
||||||
|
|
||||||
if err := useCase.tokenRepo.Create(ctx, token); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return token, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (useCase *tokenUseCase) Verify(ctx context.Context, token string) (*model.Token, error) {
|
func (useCase *tokenUseCase) Revoke(ctx context.Context, accessToken string) error {
|
||||||
return useCase.tokenRepo.Get(ctx, token)
|
if err := useCase.tokens.Remove(ctx, accessToken); err != nil {
|
||||||
}
|
return errors.Wrap(err, "failed to delete a token in the vault")
|
||||||
|
}
|
||||||
|
|
||||||
func (useCase *tokenUseCase) Revoke(ctx context.Context, token string) error {
|
return nil
|
||||||
return useCase.tokenRepo.Delete(ctx, token)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
package usecase_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/brianvoe/gofakeit"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"source.toby3d.me/website/oauth/internal/model"
|
||||||
|
repository "source.toby3d.me/website/oauth/internal/token/repository/memory"
|
||||||
|
"source.toby3d.me/website/oauth/internal/token/usecase"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestVerify(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
require := require.New(t)
|
||||||
|
assert := assert.New(t)
|
||||||
|
store := new(sync.Map)
|
||||||
|
repo := repository.NewMemoryTokenRepository(store)
|
||||||
|
ucase := usecase.NewTokenUseCase(repo)
|
||||||
|
accessToken := &model.Token{
|
||||||
|
AccessToken: gofakeit.Password(true, true, true, true, false, 32),
|
||||||
|
Type: "Bearer",
|
||||||
|
ClientID: "https://app.example.com/",
|
||||||
|
Me: "https://user.example.net/",
|
||||||
|
Scopes: []string{"create", "update", "delete"},
|
||||||
|
Profile: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(repo.Create(context.TODO(), accessToken))
|
||||||
|
|
||||||
|
token, err := ucase.Verify(context.TODO(), accessToken.AccessToken)
|
||||||
|
require.NoError(err)
|
||||||
|
assert.Equal(accessToken, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRevoke(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
require := require.New(t)
|
||||||
|
assert := assert.New(t)
|
||||||
|
store := new(sync.Map)
|
||||||
|
repo := repository.NewMemoryTokenRepository(store)
|
||||||
|
ucase := usecase.NewTokenUseCase(repo)
|
||||||
|
accessToken := gofakeit.Password(true, true, true, true, false, 32)
|
||||||
|
|
||||||
|
require.NoError(repo.Create(context.TODO(), &model.Token{
|
||||||
|
AccessToken: accessToken,
|
||||||
|
Type: "Bearer",
|
||||||
|
ClientID: "https://app.example.com/",
|
||||||
|
Me: "https://user.example.net/",
|
||||||
|
Scopes: []string{"create", "update", "delete"},
|
||||||
|
Profile: nil,
|
||||||
|
}))
|
||||||
|
|
||||||
|
token, err := repo.Get(context.TODO(), accessToken)
|
||||||
|
require.NoError(err)
|
||||||
|
assert.NotNil(token)
|
||||||
|
|
||||||
|
require.NoError(ucase.Revoke(context.TODO(), token.AccessToken))
|
||||||
|
|
||||||
|
token, err = repo.Get(context.TODO(), token.AccessToken)
|
||||||
|
require.NoError(err)
|
||||||
|
assert.Nil(token)
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
|
||||||
|
http "github.com/valyala/fasthttp"
|
||||||
|
httputil "github.com/valyala/fasthttp/fasthttputil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Serve(handler http.RequestHandler, req *http.Request, res *http.Response) error {
|
||||||
|
ln := httputil.NewInmemoryListener()
|
||||||
|
defer ln.Close()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := http.Serve(ln, handler); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
client := http.Client{
|
||||||
|
Dial: func(addr string) (net.Conn, error) {
|
||||||
|
return ln.Dial()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return client.Do(req, res)
|
||||||
|
}
|
Loading…
Reference in New Issue