321 lines
9.4 KiB
Go
321 lines
9.4 KiB
Go
// Vikunja is a to-do list application to facilitate your life.
|
|
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public Licensee as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public Licensee for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public Licensee
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
package auth
|
|
|
|
import (
|
|
"net/http"
|
|
"time"
|
|
|
|
"xorm.io/xorm"
|
|
|
|
"code.vikunja.io/api/pkg/config"
|
|
"code.vikunja.io/api/pkg/models"
|
|
"code.vikunja.io/api/pkg/user"
|
|
"code.vikunja.io/web"
|
|
"github.com/dgrijalva/jwt-go"
|
|
petname "github.com/dustinkirkland/golang-petname"
|
|
"github.com/labstack/echo/v4"
|
|
"github.com/labstack/echo/v4/middleware"
|
|
)
|
|
|
|
// These are all valid auth types
|
|
type AuthType int
|
|
|
|
const (
|
|
AuthTypeUnknown AuthType = iota
|
|
AuthTypeUser
|
|
AuthTypeLinkShare
|
|
AuthTypeIAPUser
|
|
)
|
|
|
|
const authTokenContextKey string = "authToken"
|
|
|
|
// Token represents an authentification token in signed string form
|
|
type Token struct {
|
|
Token string `json:"token"`
|
|
}
|
|
|
|
// Claims made in the JWT token for various auth types
|
|
// Only the auth module should introspect and handle claims
|
|
type AuthClaims struct {
|
|
// Common to all claims
|
|
Type AuthType `json:"type"`
|
|
|
|
// AuthTypeUser and AuthTypeIAPUser claims
|
|
UserID int64 `json:"user_id,omitempty"`
|
|
UserUsername string `json:"user_username,omitempty"`
|
|
UserEmail string `json:"user_email,omitempty"`
|
|
UserName string `json:"user_name,omitempty"`
|
|
|
|
UserEmailRemindersEnabled bool `json:"user_email_reminders_enabled"`
|
|
|
|
// AuthTypeLinkShare claims
|
|
ShareID int64 `json:"share_id,omitempty"`
|
|
ShareHash string `json:"share_hash,omitempty"`
|
|
ShareListID int64 `json:"share_list_id,omitempty"`
|
|
ShareRight models.Right `json:"share_right,omitempty"`
|
|
ShareSharedByID int64 `json:"share_shared_by_id,omitempty"`
|
|
|
|
// Common claims
|
|
jwt.StandardClaims
|
|
}
|
|
|
|
// An AuthProvider provides alternative methods of authentication
|
|
// In these cases, AuthClaims may contain hints to the user identity,
|
|
// but an outside source is the final source-of-truth for auth (e.g. Identity-Aware Proxy auth)
|
|
type AuthProvider interface {
|
|
GetWebAuth(echo.Context, *AuthClaims) (web.Auth, error)
|
|
}
|
|
|
|
var authProviders = map[AuthType]AuthProvider{}
|
|
|
|
func RegisterAuthProvider(t AuthType, provider AuthProvider) {
|
|
authProviders[t] = provider
|
|
}
|
|
|
|
// NewTokenResponse creates a new token response from a token
|
|
func NewTokenResponse(t string, c echo.Context) error {
|
|
return c.JSON(http.StatusOK, Token{Token: t})
|
|
}
|
|
|
|
// NewUserAuthTokenResponse creates a new user auth token response from a user object.
|
|
func NewUserAuthTokenResponse(u *user.User, c echo.Context) error {
|
|
t, err := NewUserJWTAuthtoken(u)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return NewTokenResponse(t, c)
|
|
}
|
|
|
|
// NewUserJWTAuthtoken generates and signes a new jwt token for a user. This is a global function to be able to call it from integration tests.
|
|
func NewUserJWTAuthtoken(u *user.User) (token string, err error) {
|
|
// Set claims
|
|
claims := &AuthClaims{
|
|
Type: AuthTypeUser,
|
|
UserID: u.ID,
|
|
UserUsername: u.Username,
|
|
UserEmail: u.Email,
|
|
UserName: u.Name,
|
|
UserEmailRemindersEnabled: u.EmailRemindersEnabled,
|
|
StandardClaims: jwt.StandardClaims{
|
|
ExpiresAt: time.Now().Add(time.Hour * 72).Unix(),
|
|
},
|
|
}
|
|
|
|
t := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
|
|
|
// Generate encoded token and send it as response.
|
|
return t.SignedString([]byte(config.ServiceJWTSecret.GetString()))
|
|
}
|
|
|
|
// NewLinkShareJWTAuthtoken creates a new jwt token from a link share
|
|
func NewLinkShareJWTAuthtoken(share *models.LinkSharing) (token string, err error) {
|
|
// Set claims
|
|
claims := &AuthClaims{
|
|
Type: AuthTypeLinkShare,
|
|
ShareID: share.ID,
|
|
ShareHash: share.Hash,
|
|
ShareListID: share.ListID,
|
|
ShareRight: share.Right,
|
|
ShareSharedByID: share.SharedByID,
|
|
StandardClaims: jwt.StandardClaims{
|
|
ExpiresAt: time.Now().Add(time.Hour * 72).Unix(),
|
|
},
|
|
}
|
|
|
|
t := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
|
|
|
// Generate encoded token and send it as response.
|
|
return t.SignedString([]byte(config.ServiceJWTSecret.GetString()))
|
|
}
|
|
|
|
// GetAuthFromClaims returns a web.Auth object from jwt claims or from an
|
|
// alternative authProvider
|
|
func GetAuthFromClaims(c echo.Context) (a web.Auth, err error) {
|
|
jwtinf := c.Get(authTokenContextKey).(*jwt.Token)
|
|
claims := jwtinf.Claims.(*AuthClaims)
|
|
if claims.Type == AuthTypeLinkShare && config.ServiceEnableLinkSharing.GetBool() {
|
|
return getLinkShareFromClaims(claims)
|
|
}
|
|
if claims.Type == AuthTypeUser {
|
|
return getUserFromClaims(claims), nil
|
|
}
|
|
if authProvider, ok := authProviders[claims.Type]; ok {
|
|
return authProvider.GetWebAuth(c, claims)
|
|
}
|
|
return nil, echo.NewHTTPError(http.StatusBadRequest, models.Message{Message: "Invalid JWT token."})
|
|
}
|
|
|
|
// getLinkShareFromClaims builds a link sharing object from jwt claims
|
|
func getLinkShareFromClaims(claims *AuthClaims) (share *models.LinkSharing, err error) {
|
|
share = &models.LinkSharing{}
|
|
share.ID = claims.ShareID
|
|
share.Hash = claims.ShareHash
|
|
share.ListID = claims.ShareListID
|
|
share.Right = claims.ShareRight
|
|
share.SharedByID = claims.ShareSharedByID
|
|
|
|
if share.Hash == "" {
|
|
return nil, echo.NewHTTPError(http.StatusBadRequest, models.Message{Message: "Invalid JWT token."})
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// getUserFromClaims Returns a new user from jwt claims
|
|
func getUserFromClaims(claims *AuthClaims) (u *user.User) {
|
|
u = &user.User{
|
|
ID: claims.UserID,
|
|
Email: claims.UserEmail,
|
|
Username: claims.UserUsername,
|
|
Name: claims.UserName,
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// GetCurrentUser returns the current user based on its jwt token
|
|
func GetCurrentUser(c echo.Context) (u *user.User, err error) {
|
|
auth, err := GetAuthFromClaims(c)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
u, ok := auth.(*user.User)
|
|
if !ok {
|
|
return nil, user.ErrCouldNotGetUserID{}
|
|
}
|
|
|
|
return u, nil
|
|
}
|
|
|
|
// Generates a new jwt token for the types AuthTypeLinkShare and AuthTypeUser
|
|
func RenewToken(s *xorm.Session, c echo.Context) (token string, err error) {
|
|
jwtinf := c.Get(authTokenContextKey).(*jwt.Token)
|
|
claims := jwtinf.Claims.(*AuthClaims)
|
|
|
|
if claims.Type == AuthTypeLinkShare {
|
|
oldShare, err := getLinkShareFromClaims(claims)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
share := &models.LinkSharing{}
|
|
share.ID = oldShare.ID
|
|
err = share.ReadOne(s, share)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return NewLinkShareJWTAuthtoken(share)
|
|
}
|
|
if claims.Type == AuthTypeUser {
|
|
oldUser := getUserFromClaims(claims)
|
|
u, err := user.GetUserWithEmail(s, &user.User{ID: oldUser.ID})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return NewUserJWTAuthtoken(u)
|
|
}
|
|
return "", echo.NewHTTPError(http.StatusBadRequest, models.Message{Message: "Invalid JWT token."})
|
|
}
|
|
|
|
// GetJWTConfig returns the config for the default JWT middleware
|
|
func GetJWTConfig() middleware.JWTConfig {
|
|
return middleware.JWTConfig{
|
|
SigningKey: []byte(config.ServiceJWTSecret.GetString()),
|
|
SigningMethod: middleware.AlgorithmHS256,
|
|
ContextKey: authTokenContextKey,
|
|
Claims: &AuthClaims{},
|
|
}
|
|
}
|
|
|
|
// GetOrCreateUserFromExternalAuth returns a user after finding or creating a matching user for the provided details
|
|
func GetOrCreateUserFromExternalAuth(s *xorm.Session, issuer, subject, email, name, preferredUsername string) (u *user.User, err error) {
|
|
if issuer == "" || subject == "" || email == "" {
|
|
return nil, echo.NewHTTPError(http.StatusBadRequest, models.Message{Message: "Missing required data."})
|
|
}
|
|
|
|
// Check if the user exists for that issuer and subject
|
|
u, err = user.GetUserWithEmail(s, &user.User{
|
|
Issuer: issuer,
|
|
Subject: subject,
|
|
})
|
|
if err != nil && !user.IsErrUserDoesNotExist(err) {
|
|
return nil, err
|
|
}
|
|
|
|
// If no user exists, create one with the preferred username if it is not already taken
|
|
if user.IsErrUserDoesNotExist(err) {
|
|
uu := &user.User{
|
|
Username: preferredUsername,
|
|
Email: email,
|
|
IsActive: true,
|
|
Issuer: issuer,
|
|
Subject: subject,
|
|
}
|
|
|
|
// Check if we actually have a preferred username and generate a random one right away if we don't
|
|
if uu.Username == "" {
|
|
uu.Username = petname.Generate(3, "-")
|
|
}
|
|
|
|
u, err = user.CreateUser(s, uu)
|
|
if err != nil && !user.IsErrUsernameExists(err) {
|
|
return nil, err
|
|
}
|
|
|
|
// If their preferred username is already taken, create some random one from the email and subject
|
|
if user.IsErrUsernameExists(err) {
|
|
uu.Username = petname.Generate(3, "-")
|
|
u, err = user.CreateUser(s, uu)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// And create its namespace
|
|
err = models.CreateNewNamespaceForUser(s, u)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// If it exists, check if the email address changed and change it if not
|
|
if email != u.Email || (name != "" && name != u.Name) {
|
|
if email != u.Email {
|
|
u.Email = email
|
|
}
|
|
if name != "" && name != u.Name {
|
|
u.Name = name
|
|
}
|
|
u, err = user.UpdateUser(s, &user.User{
|
|
ID: u.ID,
|
|
Email: u.Email,
|
|
Name: u.Name,
|
|
Issuer: issuer,
|
|
Subject: subject,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|