Skip to content

Commit

Permalink
Merge pull request #131 from containerish/email-verification-fix
Browse files Browse the repository at this point in the history
Feat: email verification
  • Loading branch information
guacamole authored Mar 30, 2022
2 parents d00c252 + cd8caf6 commit 811d4f5
Show file tree
Hide file tree
Showing 27 changed files with 356 additions and 128 deletions.
5 changes: 5 additions & 0 deletions auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"time"

"github.com/containerish/OpenRegistry/config"
"github.com/containerish/OpenRegistry/services/email"
"github.com/containerish/OpenRegistry/store/postgres"
"github.com/containerish/OpenRegistry/telemetry"
gh "github.com/google/go-github/v42/github"
Expand All @@ -27,6 +28,7 @@ type Authentication interface {
SignOut(ctx echo.Context) error
ReadUserWithSession(ctx echo.Context) error
RenewAccessToken(ctx echo.Context) error
VerifyEmail(ctx echo.Context) error
}

// New is the constructor function returns an Authentication implementation
Expand All @@ -44,6 +46,7 @@ func New(
}

ghClient := gh.NewClient(nil)
emailClient := email.New(c.Email, c.Endpoint())

a := &auth{
c: c,
Expand All @@ -52,6 +55,7 @@ func New(
github: githubOAuth,
ghClient: ghClient,
oauthStateStore: make(map[string]time.Time),
emailClient: emailClient,
}

go a.StateTokenCleanup()
Expand All @@ -67,6 +71,7 @@ type (
ghClient *gh.Client
oauthStateStore map[string]time.Time
c *config.OpenRegistryConfig
emailClient email.MailService
}
)

Expand Down
27 changes: 0 additions & 27 deletions auth/basic_auth.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package auth

import (
"context"
"encoding/base64"
"fmt"
"net/http"
Expand Down Expand Up @@ -172,29 +171,3 @@ 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) getUserWithGithubOauthToken(ctx context.Context, token string) (*types.User, error) {
req, err := a.ghClient.NewRequest(http.MethodGet, "/user", nil)
if err != nil {
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 nil, fmt.Errorf("GH_AUTH_ERROR: %w", err)
}

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

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

return user, nil
}
26 changes: 26 additions & 0 deletions auth/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,29 @@ func (a *auth) createCookie(name string, value string, httpOnly bool, expiresAt
}
return cookie
}

// makes an http request to get user info from token, if it's valid, it's all good :)
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 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 nil, fmt.Errorf("GH_AUTH_ERROR: %w", err)
}

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

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

return user, nil
}
2 changes: 1 addition & 1 deletion auth/jwt.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ func (a *auth) createServiceClaims(u types.User) ServiceClaims {
StandardClaims: jwt.StandardClaims{
Audience: a.c.Endpoint(),
ExpiresAt: time.Now().Add(time.Hour * 750).Unix(),
Id: uuid.NewString(),
Id: u.Id,
IssuedAt: time.Now().Unix(),
Issuer: a.c.Endpoint(),
NotBefore: time.Now().Unix(),
Expand Down
6 changes: 6 additions & 0 deletions auth/signin.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ func (a *auth) SignIn(ctx echo.Context) error {
})
}

if !userFromDb.IsActive {
return ctx.JSON(http.StatusUnauthorized, echo.Map{
"error": "account is inactive, please check your email and verify your account",
})
}

if !a.verifyPassword(userFromDb.Password, user.Password) {
errMsg := fmt.Errorf("invalid password")
a.logger.Log(ctx, errMsg)
Expand Down
43 changes: 23 additions & 20 deletions auth/signup.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"time"
"unicode"

"github.com/containerish/OpenRegistry/config"
"github.com/containerish/OpenRegistry/types"
"github.com/google/uuid"
"github.com/labstack/echo/v4"
Expand Down Expand Up @@ -58,6 +59,10 @@ func (a *auth) SignUp(ctx echo.Context) error {
Id: uuid.NewString(),
}

if a.c.Environment == config.CI {
newUser.IsActive = true
}

err = a.pgStore.AddUser(ctx.Request().Context(), newUser)
if err != nil {
a.logger.Log(ctx, err)
Expand All @@ -66,40 +71,38 @@ func (a *auth) SignUp(ctx echo.Context) error {
})
}

accessToken, err := a.newWebLoginToken(newUser.Id, newUser.Username, "access")
if err != nil {
// in case of CI setup, no need to send verification emails
if a.c.Environment == config.CI {
a.logger.Log(ctx, err)
return ctx.JSON(http.StatusInternalServerError, echo.Map{
"error": err.Error(),
"code": "CREATE_NEW_ACCESS_TOKEN",
return ctx.JSON(http.StatusCreated, echo.Map{
"message": "user successfully created",
})
}
refreshToken, err := a.newWebLoginToken(newUser.Id, newUser.Username, "refresh")

token, err := a.newWebLoginToken(newUser.Id, newUser.Username, "access")
if err != nil {
a.logger.Log(ctx, err)
return ctx.JSON(http.StatusInternalServerError, echo.Map{
"error": err.Error(),
"code": "CREATE_NEW_REFRESH_TOKEN",
"code": "CREATE_NEW_ACCESS_TOKEN",
})
}

id := uuid.NewString()
if err = a.pgStore.AddSession(ctx.Request().Context(), id, refreshToken, newUser.Username); err != nil {
a.logger.Log(ctx, err)
return ctx.JSON(http.StatusBadRequest, echo.Map{
err = a.pgStore.AddVerifyEmail(ctx.Request().Context(), token, newUser.Id)
if err != nil {
ctx.Set(types.HttpEndpointErrorKey, err.Error())
return ctx.JSON(http.StatusInternalServerError, echo.Map{
"error": err.Error(),
"message": "ERR_CREATING_SESSION",
"message": "ERR_ADDING_VERIFY_EMAIL",
})
}

sessionId := fmt.Sprintf("%s:%s", id, newUser.Id)
sessionCookie := a.createCookie("session_id", sessionId, 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)
if err = a.emailClient.SendEmail(newUser, token); err != nil {
ctx.Set(types.HttpEndpointErrorKey, err.Error())
return ctx.JSON(http.StatusInternalServerError, echo.Map{
"error": err.Error(),
})
}

a.logger.Log(ctx, err)
return ctx.JSON(http.StatusCreated, echo.Map{
Expand Down
93 changes: 93 additions & 0 deletions auth/verify_email.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package auth

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

"github.com/containerish/OpenRegistry/types"
"github.com/golang-jwt/jwt"
"github.com/labstack/echo/v4"
)

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

token := ctx.QueryParam("token")
if token == "" {
return ctx.JSON(http.StatusBadRequest, echo.Map{
"error": "EMPTY_TOKEN",
})
}

jToken, err := base64.StdEncoding.DecodeString(token)
if err != nil {
return ctx.JSON(http.StatusBadRequest, echo.Map{
"error": err.Error(),
"msg": "EMPTY_TOKEN",
})
}

var t *jwt.Token

_, err = jwt.Parse(string(jToken), func(jt *jwt.Token) (interface{}, error) {
if jt == nil {
return nil, fmt.Errorf("ERR_PARSE_JWT_TOKEN")
}
t = jt
return nil, nil
})

claims, ok := t.Claims.(jwt.MapClaims)
if !ok {
ctx.Set(types.HttpEndpointErrorKey, err.Error())
return ctx.JSON(http.StatusBadRequest, echo.Map{
"error": err.Error(),
"msg": "ERR_CONVERT_CLAIMS",
})
}

id := claims["ID"].(string)
tokenFromDb, err := a.pgStore.GetVerifyEmail(ctx.Request().Context(), id)
if tokenFromDb != string(jToken) {
ctx.Set(types.HttpEndpointErrorKey, err.Error())
return ctx.JSON(http.StatusInternalServerError, echo.Map{
"error": err.Error(),
"msg": "ERR_TOKEN_MISMATCH",
})
}

user, err := a.pgStore.GetUserById(ctx.Request().Context(), id)
if err != nil {
ctx.Set(types.HttpEndpointErrorKey, err.Error())
return ctx.JSON(http.StatusInternalServerError, echo.Map{
"error": err.Error(),
"msg": "USER_NOT_FOUND",
})
}

user.IsActive = true

err = a.pgStore.UpdateUser(ctx.Request().Context(), id, user)
if err != nil {
ctx.Set(types.HttpEndpointErrorKey, err.Error())
return ctx.JSON(http.StatusInternalServerError, echo.Map{
"error": err.Error(),
"msg": "ERROR_UPDATE_USER",
})
}

err = a.pgStore.DeleteVerifyEmail(ctx.Request().Context(), id)
if err != nil {
ctx.Set(types.HttpEndpointErrorKey, err.Error())
return ctx.JSON(http.StatusInternalServerError, echo.Map{
"error": err.Error(),
"msg": "ERROR_DELETE_VERIFY_EMAIL",
})
}

return ctx.JSON(http.StatusOK, echo.Map{
"message": "success",
})
}
6 changes: 6 additions & 0 deletions config.yaml.example
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,9 @@ log_service:
auth_method: basic_auth
username: grafana-username
password: grafana-password
email:
enabled: true
api_key: <sendgrid-api-key>
  send_as: admin@openregistry.dev
  verify_template_id: <verify_template_id>
  forgot_password_template_id: <forgot_password_template_id>
9 changes: 9 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type (
WebAppEndpoint string `mapstructure:"web_app_url"`
WebAppRedirectURL string `mapstructure:"web_app_redirect_url"`
Debug bool `mapstructure:"debug"`
Email *Email `mapstructure:"email"`
}

Registry struct {
Expand Down Expand Up @@ -68,6 +69,14 @@ type (
OAuth struct {
Github GithubOAuth `mapstructure:"github"`
}

Email struct {
ApiKey string `mapstructure:"api_key"`
SendAs string `mapstructure:"send_as"`
VerifyEmailTemplate string `mapstructure:"verify_template_id"`
WelcomeEmailTemplate string `mapstructure:"welcome_template_id"`
Enabled bool `mapstructure:"enabled"`
}
)

func (r *Registry) Address() string {
Expand Down
1 change: 1 addition & 0 deletions db/migrations/000007_create_verify_emails_table.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE IF EXISTS verify_emails;
4 changes: 4 additions & 0 deletions db/migrations/000007_create_verify_emails_table.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
CREATE TABLE "verify_emails" (
"token" text,
"user_id" uuid
)
Loading

0 comments on commit 811d4f5

Please sign in to comment.