Skip to content

Commit

Permalink
feat: Auth tokens for client-side usage
Browse files Browse the repository at this point in the history
Signed-off-by: jay-dee7 <[email protected]>
  • Loading branch information
jay-dee7 committed Dec 26, 2023
1 parent 889cab1 commit 9d63cfd
Show file tree
Hide file tree
Showing 16 changed files with 332 additions and 9 deletions.
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ certs:
openssl req -x509 -newkey rsa:4096 -keyout .certs/registry.local -out .certs/registry.local.crt -sha256 -days 365 \
-subj "/C=US/ST=Oregon/L=Portland/O=Company Name/OU=Org/CN=registry.local" -nodes

load_dummy_users:
sh ./scripts/load_dummy_users.sh

reset:
psql -c 'drop database open_registry' && \
psql -c 'drop role open_registry_user' && \
Expand Down
118 changes: 118 additions & 0 deletions api/users/users.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package users

import (
"fmt"
"net/http"
"time"

"github.com/containerish/OpenRegistry/auth"
"github.com/containerish/OpenRegistry/store/v1/types"
"github.com/containerish/OpenRegistry/store/v1/users"
"github.com/containerish/OpenRegistry/telemetry"
Expand All @@ -12,6 +15,8 @@ import (
type (
UserApi interface {
SearchUsers(echo.Context) error
CreateUserToken(ctx echo.Context) error
ListUserToken(ctx echo.Context) error
}

api struct {
Expand Down Expand Up @@ -47,3 +52,116 @@ func (a *api) SearchUsers(ctx echo.Context) error {
a.logger.Log(ctx, nil).Send()
return echoErr
}

func (a *api) CreateUserToken(ctx echo.Context) error {
ctx.Set(types.HandlerStartTime, time.Now())

user, ok := ctx.Get(string(types.UserContextKey)).(*types.User)
if !ok {
err := fmt.Errorf("missing authentication credentials")
echoErr := ctx.JSON(http.StatusUnauthorized, echo.Map{
"error": err.Error(),
})

a.logger.Log(ctx, err).Send()
return echoErr
}

var body types.CreateAuthTokenRequest
if err := ctx.Bind(&body); err != nil {
echoErr := ctx.JSON(http.StatusBadRequest, echo.Map{
"error": err.Error(),
})

a.logger.Log(ctx, err).Send()
return echoErr
}

if body.Name == "" {
err := fmt.Errorf("token name is a required field")
echoErr := ctx.JSON(http.StatusBadRequest, echo.Map{
"error": err.Error(),
})

a.logger.Log(ctx, err).Send()
return echoErr
}

token, err := types.CreateNewAuthToken()
if err != nil {
echoErr := ctx.JSON(http.StatusInternalServerError, echo.Map{
"error": err.Error(),
})

a.logger.Log(ctx, err).Send()
return echoErr
}

hashedToken, err := auth.GenerateSafeHash([]byte(token.RawString()))
if err != nil {
echoErr := ctx.JSON(http.StatusInternalServerError, echo.Map{
"error": err.Error(),
})

a.logger.Log(ctx, err).Send()
return echoErr
}

authToken := &types.AuthTokens{
CreatedAt: time.Now(),
ExpiresAt: body.ExpiresAt,
Name: body.Name,
AuthToken: hashedToken,
OwnerID: user.ID,
}

if err = a.userStore.AddAuthToken(ctx.Request().Context(), authToken); err != nil {
echoErr := ctx.JSON(http.StatusInternalServerError, echo.Map{
"error": err.Error(),
})

a.logger.Log(ctx, err).Send()
return echoErr
}

echoErr := ctx.JSON(http.StatusOK, echo.Map{
"token": token.String(),
})

a.logger.Log(ctx, nil).Str("client_token", token.String()).Str("stored_token", hashedToken).Send()
return echoErr
}

func (a *api) ListUserToken(ctx echo.Context) error {
ctx.Set(types.HandlerStartTime, time.Now())

user, ok := ctx.Get(string(types.UserContextKey)).(*types.User)
if !ok {
err := fmt.Errorf("missing authentication credentials")
echoErr := ctx.JSON(http.StatusUnauthorized, echo.Map{
"error": err.Error(),
})

a.logger.Log(ctx, err).Send()
return echoErr
}

tokens, err := a.userStore.ListAuthTokens(ctx.Request().Context(), user.ID)
if err != nil {
echoErr := ctx.JSON(http.StatusInternalServerError, echo.Map{
"error": err.Error(),
})

a.logger.Log(ctx, err).Send()
return echoErr
}

if len(tokens) == 0 {
tokens = make([]*types.AuthTokens, 0)
}

echoErr := ctx.JSON(http.StatusOK, tokens)

a.logger.Log(ctx, nil).Send()
return echoErr
}
10 changes: 10 additions & 0 deletions auth/basic_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,16 @@ func (a *auth) validateBasicAuthCredentials(auth string) (*types.User, error) {

return user, nil
}

if strings.HasPrefix(password, types.OpenRegistryAuthTokenPrefix) {
user, err := a.validateUserWithPAT(context.Background(), username, password)
if err != nil {
return nil, err
}

return user, nil
}

user, err := a.validateUser(username, password)
if err != nil {
return nil, err
Expand Down
16 changes: 14 additions & 2 deletions auth/bcrypt.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
package auth

import (
"crypto/sha256"
"fmt"

"golang.org/x/crypto/bcrypt"
)

const bcryptMinCost = 6
const BcryptMinCost = 6

func (a *auth) hashPassword(password string) (string, error) {
// Convert password string to byte slice
var passwordBytes = []byte(password)

// Hash password with Bcrypt's min cost
hashedPasswordBytes, err := bcrypt.GenerateFromPassword(passwordBytes, bcryptMinCost)
hashedPasswordBytes, err := bcrypt.GenerateFromPassword(passwordBytes, BcryptMinCost)

return string(hashedPasswordBytes), err
}
Expand All @@ -20,3 +23,12 @@ func (a *auth) verifyPassword(hashedPassword, currPassword string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(currPassword))
return err == nil
}

func GenerateSafeHash(input []byte) (string, error) {
hash := sha256.New()
if n, err := hash.Write(input); err != nil || n != len(input) {
return "", fmt.Errorf("error generating hash")
}

return fmt.Sprintf("%x", hash.Sum(nil)), nil
}
6 changes: 2 additions & 4 deletions auth/permissions.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,10 +129,8 @@ func (a *auth) handleTokenRequest(ctx echo.Context, handler echo.HandlerFunc) (e
if err := a.populateUserFromPermissionsCheck(ctx); err != nil {
registryErr := common.RegistryErrorResponse(
registry.RegistryErrorCodeUnauthorized,
"missing user credentials in request",
echo.Map{
"error": err.Error(),
},
err.Error(),
nil,
)

echoErr := ctx.JSONBlob(http.StatusUnauthorized, registryErr.Bytes())
Expand Down
2 changes: 1 addition & 1 deletion auth/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"strings"
"time"

"github.com/containerish/OpenRegistry/types"
"github.com/containerish/OpenRegistry/store/v1/types"
"github.com/labstack/echo/v4"
)

Expand Down
25 changes: 25 additions & 0 deletions auth/validate_user.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,28 @@ func (a *auth) validateUser(username, password string) (*types.User, error) {

return user, nil
}

func (a *auth) validateUserWithPAT(ctx context.Context, username, authToken string) (*types.User, error) {
user, err := a.userStore.GetUserByUsername(ctx, username)
if err != nil {
return nil, err
}

token, err := (&types.AuthToken{}).FromString(authToken)
if err != nil {
return nil, fmt.Errorf("ERR_PARSE_AUTH_TOKEN: %w", err)
}

hashedToken, err := GenerateSafeHash([]byte(token.RawString()))
if err != nil {
return nil, err
}

_, err = a.userStore.GetAuthToken(ctx, user.ID, hashedToken)
if err != nil {
return nil, err
}

return user, nil

}
8 changes: 8 additions & 0 deletions cmd/migrations/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,14 @@ func createOpenRegistryTables(ctx *cli.Context, db *bun.DB) error {
}
color.Green(`Table "permissions" created ✔︎`)

_, err = db.NewCreateTable().Model(&types.AuthTokens{}).Table().IfNotExists().Exec(ctx.Context)
if err != nil {
return errors.New(
color.RedString("Table=auth_tokens Created=❌ Error=%s", err),
)
}
color.Green(`Table "auth_tokens" created ✔︎`)

return nil
}

Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ require (
github.com/multiformats/go-multihash v0.2.3 // indirect
github.com/multiformats/go-multistream v0.5.0 // indirect
github.com/multiformats/go-varint v0.0.7 // indirect
github.com/oklog/ulid/v2 v2.1.0 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
Expand Down
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,8 @@ github.com/multiformats/go-varint v0.0.1/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXS
github.com/multiformats/go-varint v0.0.5/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE=
github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8=
github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU=
github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU=
github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4=
github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
Expand All @@ -589,6 +591,7 @@ github.com/openzipkin/zipkin-go v0.4.1 h1:kNd/ST2yLLWhaWrkgchya40TJabe8Hioj9udfP
github.com/openzipkin/zipkin-go v0.4.1/go.mod h1:qY0VqDSN1pOBN94dBc6w2GJlWLiovAyg7Qt6/I9HecM=
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/petar/GoLLRB v0.0.0-20210522233825-ae3b015fd3e9 h1:1/WtZae0yGtPq+TI6+Tv1WTxkukpXeMlviSxvL7SRgk=
Expand Down
6 changes: 4 additions & 2 deletions router/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"github.com/labstack/echo/v4"
)

func RegisterUserRoutes(router *echo.Group, api users.UserApi) {
router.Add(http.MethodGet, "/search", api.SearchUsers)
func RegisterUserRoutes(router *echo.Group, api users.UserApi, middlewares ...echo.MiddlewareFunc) {
router.Add(http.MethodGet, "/search", api.SearchUsers, middlewares...)
router.Add(http.MethodPost, "/token", api.CreateUserToken, middlewares...)
router.Add(http.MethodGet, "/token", api.ListUserToken, middlewares...)
}
14 changes: 14 additions & 0 deletions scripts/load_dummy_users.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/bin/sh

curl -sSL -XPOST -d '{"email": "[email protected]", "username": "johndoe", "password": "Qwerty@123"}' 'http://localhost:5000/auth/signup' | jq
curl -sSL -XPOST -d '{"email": "[email protected]", "username": "asta", "password": "Qwerty@123"}' 'http://localhost:5000/auth/signup' | jq
curl -sSL -XPOST -d '{"email": "[email protected]", "username": "yuno", "password": "Qwerty@123"}' 'http://localhost:5000/auth/signup' | jq
curl -sSL -XPOST -d '{"email": "[email protected]", "username": "noelle", "password": "Qwerty@123"}' 'http://localhost:5000/auth/signup' | jq
curl -sSL -XPOST -d '{"email": "[email protected]", "username": "mimosa", "password": "Qwerty@123"}' 'http://localhost:5000/auth/signup' | jq
curl -sSL -XPOST -d '{"email": "[email protected]", "username": "klaus", "password": "Qwerty@123"}' 'http://localhost:5000/auth/signup' | jq
curl -sSL -XPOST -d '{"email": "[email protected]", "username": "finral", "password": "Qwerty@123"}' 'http://localhost:5000/auth/signup' | jq
curl -sSL -XPOST -d '{"email": "[email protected]", "username": "yami", "password": "Qwerty@123"}' 'http://localhost:5000/auth/signup' | jq
curl -sSL -XPOST -d '{"email": "[email protected]", "username": "vangeance", "password": "Qwerty@123"}' 'http://localhost:5000/auth/signup' | jq
curl -sSL -XPOST -d '{"email": "[email protected]", "username": "leopold", "password": "Qwerty@123"}' 'http://localhost:5000/auth/signup' | jq
curl -sSL -XPOST -d '{"email": "[email protected]", "username": "vanessa", "password": "Qwerty@123"}' 'http://localhost:5000/auth/signup' | jq
curl -sSL -XPOST -d '{"email": "[email protected]", "username": "charmy", "password": "Qwerty@123"}' 'http://localhost:5000/auth/signup' | jq
60 changes: 60 additions & 0 deletions store/v1/types/auth_tokens.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package types

import (
"fmt"
"math/rand"
"strings"
"time"

"github.com/oklog/ulid/v2"
)

type AuthToken struct {
id ulid.ULID
}

const (
OpenRegistryAuthTokenPrefix = "oreg_pat_"
)

func (at *AuthToken) String() string {
return fmt.Sprintf("%s%s", OpenRegistryAuthTokenPrefix, at.id.String())
}

func (at *AuthToken) Bytes() []byte {
return at.id.Bytes()
}

func (at *AuthToken) Compare(other ulid.ULID) int {
return at.id.Compare(other)
}

func (at *AuthToken) FromString(token string) (*AuthToken, error) {
token = strings.TrimPrefix(token, OpenRegistryAuthTokenPrefix)

id, err := ulid.Parse(token)
if err != nil {
return nil, err
}

return &AuthToken{id: id}, nil
}

func (at *AuthToken) RawString() string {
return at.id.String()
}

func CreateNewAuthToken() (*AuthToken, error) {
now := time.Now()
entropy := rand.New(rand.NewSource(now.UnixNano()))

Check failure on line 49 in store/v1/types/auth_tokens.go

View workflow job for this annotation

GitHub Actions / lint

G404: Use of weak random number generator (math/rand instead of crypto/rand) (gosec)
ms := ulid.Timestamp(now)

id, err := ulid.New(ms, entropy)
if err != nil {
return nil, err
}

return &AuthToken{
id: id,
}, nil
}
Loading

0 comments on commit 9d63cfd

Please sign in to comment.