Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ migup:
migdown:
migrate -database ${POSTGRESQL_URL} -path db/migrations down

cleanup: migdown migup

mock-images:
bash ./scripts/mock-images.sh

tools:
pip3 install ggshield pre-commit
pre-commit install

5 changes: 5 additions & 0 deletions auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,14 @@ type Authentication interface {
BasicAuth() echo.MiddlewareFunc
Token(ctx echo.Context) error
JWT() echo.MiddlewareFunc
JWTRest() echo.MiddlewareFunc
ACL() echo.MiddlewareFunc
LoginWithGithub(ctx echo.Context) error
GithubLoginCallbackHandler(ctx echo.Context) error
ExpireSessions(ctx echo.Context) error
SignOut(ctx echo.Context) error
ReadUserWithSession(ctx echo.Context) error
RenewAccessToken(ctx echo.Context) error
}

// New is the constructor function returns an Authentication implementation
Expand Down
15 changes: 8 additions & 7 deletions auth/basic_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,26 +174,27 @@ func BasicAuthWithConfig(config middleware.BasicAuthConfig) echo.MiddlewareFunc
}

// makes an http request to get user info from token, if it's valid, it's all good :)
func (a *auth) validateUserWithGithubOauthToken(ctx context.Context, token string) (bool, error) {
func (a *auth) getUserWithGithubOauthToken(ctx context.Context, token string) (*types.User, error) {
req, err := a.ghClient.NewRequest(http.MethodGet, "/user", nil)
if err != nil {
return false, fmt.Errorf("GH_AUTH_REQUEST_ERROR: %w", err)
return nil, fmt.Errorf("GH_AUTH_REQUEST_ERROR: %w", err)
}
req.Header.Set(AuthorizationHeaderKey, "token "+token)

var oauthUser types.User
resp, err := a.ghClient.Do(ctx, req, &oauthUser)
if err != nil {
return false, fmt.Errorf("GH_AUTH_ERROR: %w", err)
return nil, fmt.Errorf("GH_AUTH_ERROR: %w", err)
}

if resp.StatusCode != http.StatusOK {
return false, fmt.Errorf("UNAUTHORIZED")
return nil, fmt.Errorf("GHO_UNAUTHORIZED")
}

if _, err = a.pgStore.GetUser(ctx, oauthUser.Email); err != nil {
return false, fmt.Errorf("PG_GET_USER_ERR: %w", err)
user, err := a.pgStore.GetUser(ctx, oauthUser.Email, false)
if err != nil {
return nil, fmt.Errorf("PG_GET_USER_ERR: %w", err)
}

return true, nil
return user, nil
}
83 changes: 48 additions & 35 deletions auth/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ package auth

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

"github.com/containerish/OpenRegistry/config"

"github.com/containerish/OpenRegistry/types"
"github.com/google/uuid"
"github.com/labstack/echo/v4"
Expand Down Expand Up @@ -78,49 +80,31 @@ func (a *auth) GithubLoginCallbackHandler(ctx echo.Context) error {
})
}

secure := true
sameSite := http.SameSiteStrictMode
domain := strings.TrimPrefix(a.c.WebAppEndpoint, "https://")
if a.c.Environment == config.Local {
secure = false
sameSite = http.SameSiteLaxMode
domain = "localhost"
}

accessCookie := &http.Cookie{
Name: "access",
Value: accessToken,
Path: "/",
Domain: domain,
Expires: time.Now().Add(time.Hour),
MaxAge: AccessCookieMaxAge,
Secure: secure,
SameSite: sameSite,
HttpOnly: true,
}

refreshCookie := &http.Cookie{
Name: "refresh",
Value: refreshToken,
Path: "/",
Domain: domain,
Expires: time.Now().Add(time.Hour * 750),
MaxAge: RefreshCookieMaxAge,
Secure: secure,
SameSite: sameSite,
HttpOnly: true,
}

if err := a.pgStore.AddOAuthUser(ctx.Request().Context(), &oauthUser); err != nil {
oauthUser.Password = refreshToken
if err = a.pgStore.AddOAuthUser(ctx.Request().Context(), &oauthUser); err != nil {
ctx.Set(types.HttpEndpointErrorKey, err.Error())
return ctx.JSON(http.StatusInternalServerError, echo.Map{
"error": err.Error(),
"code": "GH_OAUTH_STORE_OAUTH_USER",
})
}

sessionId := uuid.NewString()
if err = a.pgStore.AddSession(ctx.Request().Context(), sessionId, refreshToken, oauthUser.Username); err != nil {
return ctx.JSON(http.StatusBadRequest, echo.Map{
"error": err.Error(),
"message": "ERR_CREATING_SESSION",
})
}
val := fmt.Sprintf("%s:%s", sessionId, oauthUser.Id)

sessionCookie := a.createCookie("session_id", val, false, time.Now().Add(time.Hour*750))
accessCookie := a.createCookie("access", accessToken, true, time.Now().Add(time.Hour))
refreshCookie := a.createCookie("refresh", refreshToken, true, time.Now().Add(time.Hour*750))

ctx.SetCookie(accessCookie)
ctx.SetCookie(refreshCookie)
ctx.SetCookie(sessionCookie)
a.logger.Log(ctx, nil)
return ctx.Redirect(http.StatusTemporaryRedirect, a.c.WebAppRedirectURL)
}
Expand All @@ -129,3 +113,32 @@ const (
AccessCookieMaxAge = int(time.Second * 3600)
RefreshCookieMaxAge = int(AccessCookieMaxAge * 3600)
)

func (a *auth) createCookie(name string, value string, httpOnly bool, expiresAt time.Time) *http.Cookie {

secure := true
sameSite := http.SameSiteStrictMode
if a.c.Environment == config.Local {
secure = false
sameSite = http.SameSiteLaxMode
}

webappEndpoint := a.c.WebAppEndpoint
if a.c.Environment == config.Local {
host, _, err := net.SplitHostPort(webappEndpoint)
if err != nil {
webappEndpoint = host
}
}
cookie := &http.Cookie{
Name: name,
Value: value,
Path: "/",
Domain: webappEndpoint,
Expires: expiresAt,
Secure: secure,
SameSite: sameSite,
HttpOnly: httpOnly,
}
return cookie
}
122 changes: 65 additions & 57 deletions auth/jwt.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@ import (

type Claims struct {
jwt.StandardClaims
Type string
Access AccessList
}

type PlatformClaims struct {
OauthPayload *oauth2.Token `json:"oauth2_token,omitempty"`
jwt.StandardClaims
UserPayload types.User
Type string
}

type RefreshClaims struct {
Expand All @@ -31,9 +32,17 @@ type ServiceClaims struct {
Access AccessList
}

//
func (a *auth) newPublicPullToken() (string, error) {
tokenLife := time.Now().Add(time.Hour * 24 * 14).Unix()
claims := a.createClaims("public_pull_user", "", tokenLife)
acl := AccessList{
{
Type: "repository",
Name: "*/*",
Actions: []string{"pull"},
},
}

claims := a.createClaims("public_pull_user", "", acl)

// TODO (jay-dee7)- handle this properly, check for errors and don't set defaults for actions
claims.Access[0].Actions = []string{"pull"}
Expand All @@ -55,7 +64,7 @@ func (a *auth) SignOAuthToken(u types.User, payload *oauth2.Token) (string, stri

func (a *auth) newOAuthToken(u types.User, payload *oauth2.Token) (string, string, error) {
accessClaims := a.createOAuthClaims(u, payload)
refreshClaims := a.createRefreshClaims(u)
refreshClaims := a.createRefreshClaims(u.Id)

accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, &accessClaims)
accessSign, err := accessToken.SignedString([]byte(a.c.Registry.SigningSecret))
Expand All @@ -75,37 +84,40 @@ func (a *auth) newOAuthToken(u types.User, payload *oauth2.Token) (string, strin

//nolint
func (a *auth) newServiceToken(u types.User) (string, error) {
u.StripForToken()
claims := a.createServiceClaims(u)
acl := AccessList{
{
Type: "repository",
Name: fmt.Sprintf("%s/*", u.Username),
Actions: []string{"push", "pull"},
},
}
claims := a.createClaims(u.Id, "service", acl)

token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
sign, err := token.SignedString(a.c.Registry.SigningSecret)
sign, err := token.SignedString([]byte(a.c.Registry.SigningSecret))
if err != nil {
return "", err
return "", fmt.Errorf("error signing secret %w", err)
}

return sign, nil
}

func (a *auth) newWebLoginToken(u types.User) (string, string, error) {
u.StripForToken()
claims := a.createWebLoginClaims(u)
refreshClaims := a.createRefreshClaims(u)

rawAccess := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
rawRefresh := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims)

accessToken, err := rawAccess.SignedString([]byte(a.c.Registry.SigningSecret))
if err != nil {
return "", "", err
func (a *auth) newWebLoginToken(userId, username, tokenType string) (string, error) {
acl := AccessList{
{
Type: "repository",
Name: fmt.Sprintf("%s/*", username),
Actions: []string{"push", "pull"},
},
}

refreshToken, err := rawRefresh.SignedString([]byte(a.c.Registry.SigningSecret))
claims := a.createClaims(userId, tokenType, acl)
raw := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
token, err := raw.SignedString([]byte(a.c.Registry.SigningSecret))
if err != nil {
return "", "", err
return "", err
}

return accessToken, refreshToken, nil
return token, nil
}

//nolint
Expand Down Expand Up @@ -142,7 +154,6 @@ func (a *auth) createServiceClaims(u types.User) ServiceClaims {
// },
func (a *auth) createOAuthClaims(u types.User, token *oauth2.Token) PlatformClaims {
claims := PlatformClaims{
UserPayload: u,
OauthPayload: token,
StandardClaims: jwt.StandardClaims{
Audience: a.c.Endpoint(),
Expand All @@ -158,44 +169,35 @@ func (a *auth) createOAuthClaims(u types.User, token *oauth2.Token) PlatformClai
return claims
}

func (a *auth) createRefreshClaims(u types.User) RefreshClaims {
func (a *auth) createRefreshClaims(userId string) RefreshClaims {
claims := RefreshClaims{
ID: u.Id,
ID: userId,
StandardClaims: jwt.StandardClaims{
Audience: a.c.Endpoint(),
ExpiresAt: time.Now().Add(time.Hour * 750).Unix(), // Refresh tokens can live longer
Id: uuid.NewString(),
Id: userId,
IssuedAt: time.Now().Unix(),
Issuer: a.c.Endpoint(),
NotBefore: time.Now().Unix(),
Subject: u.Id,
Subject: userId,
},
}

return claims
}

func (a *auth) createWebLoginClaims(u types.User) PlatformClaims {
claims := PlatformClaims{
UserPayload: u,
StandardClaims: jwt.StandardClaims{
Audience: a.c.Endpoint(),
ExpiresAt: time.Now().Add(time.Hour).Unix(),
Id: uuid.NewString(),
IssuedAt: time.Now().Unix(),
Issuer: a.c.Endpoint(),
NotBefore: time.Now().Unix(),
Subject: u.Id,
},
}

return claims
}

func (a *auth) newToken(u types.User, tokenLife int64) (string, error) {
func (a *auth) newToken(u *types.User) (string, error) {
//for now we're sending same name for sub and name.
//TODO when repositories need collaborators
claims := a.createClaims(u.Username, u.Username, tokenLife)

acl := AccessList{
{
Type: "repository",
Name: fmt.Sprintf("%s/*", u.Username),
Actions: []string{"push", "pull"},
},
}
claims := a.createClaims(u.Id, "access", acl)
token := jwt.NewWithClaims(jwt.SigningMethodHS256, &claims)

// Generate encoded token and send it as response.
Expand Down Expand Up @@ -231,24 +233,30 @@ claims format
}
*/

func (a *auth) createClaims(sub, name string, tokenLife int64) Claims {
func (a *auth) createClaims(id, tokenType string, acl AccessList) Claims {

var tokenLife int64
switch tokenType {
case "access":
tokenLife = time.Now().Add(time.Hour).Unix()
case "refresh":
tokenLife = time.Now().Add(time.Hour * 750).Unix()
case "service":
tokenLife = time.Now().Add(time.Hour * 750).Unix()
}

claims := Claims{
StandardClaims: jwt.StandardClaims{
Audience: a.c.Endpoint(),
ExpiresAt: tokenLife,
Id: uuid.NewString(),
Id: id,
IssuedAt: time.Now().Unix(),
Issuer: a.c.Endpoint(),
NotBefore: time.Now().Unix(),
Subject: sub,
},
Access: AccessList{
{
Type: "repository",
Name: fmt.Sprintf("%s/*", name),
Actions: []string{"push", "pull"},
},
Subject: id,
},
Access: acl,
Type: tokenType,
}
return claims
}
Expand Down
Loading