123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410 |
- package v1
- import (
- "context"
- "encoding/json"
- "fmt"
- "net/http"
- "regexp"
- "strings"
- "time"
- "github.com/labstack/echo/v4"
- "github.com/pkg/errors"
- "golang.org/x/crypto/bcrypt"
- "github.com/usememos/memos/api/auth"
- "github.com/usememos/memos/internal/util"
- "github.com/usememos/memos/plugin/idp"
- "github.com/usememos/memos/plugin/idp/oauth2"
- storepb "github.com/usememos/memos/proto/gen/store"
- "github.com/usememos/memos/store"
- )
- var (
- usernameMatcher = regexp.MustCompile("^[a-z0-9]([a-z0-9-]{1,30}[a-z0-9])$")
- )
- type SignIn struct {
- Username string `json:"username"`
- Password string `json:"password"`
- Remember bool `json:"remember"`
- }
- type SSOSignIn struct {
- IdentityProviderID int32 `json:"identityProviderId"`
- Code string `json:"code"`
- RedirectURI string `json:"redirectUri"`
- }
- type SignUp struct {
- Username string `json:"username"`
- Password string `json:"password"`
- }
- func (s *APIV1Service) registerAuthRoutes(g *echo.Group) {
- g.POST("/auth/signin", s.SignIn)
- g.POST("/auth/signin/sso", s.SignInSSO)
- g.POST("/auth/signout", s.SignOut)
- g.POST("/auth/signup", s.SignUp)
- }
- // SignIn godoc
- //
- // @Summary Sign-in to memos.
- // @Tags auth
- // @Accept json
- // @Produce json
- // @Param body body SignIn true "Sign-in object"
- // @Success 200 {object} store.User "User information"
- // @Failure 400 {object} nil "Malformatted signin request"
- // @Failure 401 {object} nil "Password login is deactivated | Incorrect login credentials, please try again"
- // @Failure 403 {object} nil "User has been archived with username %s"
- // @Failure 500 {object} nil "Failed to find system setting | Failed to unmarshal system setting | Incorrect login credentials, please try again | Failed to generate tokens | Failed to create activity"
- // @Router /api/v1/auth/signin [POST]
- func (s *APIV1Service) SignIn(c echo.Context) error {
- ctx := c.Request().Context()
- signin := &SignIn{}
- disablePasswordLoginSystemSetting, err := s.Store.GetSystemSetting(ctx, &store.FindSystemSetting{
- Name: SystemSettingDisablePasswordLoginName.String(),
- })
- if err != nil {
- return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting").SetInternal(err)
- }
- if disablePasswordLoginSystemSetting != nil {
- disablePasswordLogin := false
- err = json.Unmarshal([]byte(disablePasswordLoginSystemSetting.Value), &disablePasswordLogin)
- if err != nil {
- return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal system setting").SetInternal(err)
- }
- if disablePasswordLogin {
- return echo.NewHTTPError(http.StatusUnauthorized, "Password login is deactivated")
- }
- }
- if err := json.NewDecoder(c.Request().Body).Decode(signin); err != nil {
- return echo.NewHTTPError(http.StatusBadRequest, "Malformatted signin request").SetInternal(err)
- }
- user, err := s.Store.GetUser(ctx, &store.FindUser{
- Username: &signin.Username,
- })
- if err != nil {
- return echo.NewHTTPError(http.StatusInternalServerError, "Incorrect login credentials, please try again")
- }
- if user == nil {
- return echo.NewHTTPError(http.StatusUnauthorized, "Incorrect login credentials, please try again")
- } else if user.RowStatus == store.Archived {
- return echo.NewHTTPError(http.StatusForbidden, fmt.Sprintf("User has been archived with username %s", signin.Username))
- }
- // Compare the stored hashed password, with the hashed version of the password that was received.
- if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(signin.Password)); err != nil {
- // If the two passwords don't match, return a 401 status.
- return echo.NewHTTPError(http.StatusUnauthorized, "Incorrect login credentials, please try again")
- }
- var expireAt time.Time
- if !signin.Remember {
- expireAt = time.Now().Add(auth.AccessTokenDuration)
- }
- accessToken, err := auth.GenerateAccessToken(user.Username, user.ID, expireAt, []byte(s.Secret))
- if err != nil {
- return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to generate tokens, err: %s", err)).SetInternal(err)
- }
- if err := s.UpsertAccessTokenToStore(ctx, user, accessToken); err != nil {
- return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to upsert access token, err: %s", err)).SetInternal(err)
- }
- cookieExp := time.Now().Add(auth.CookieExpDuration)
- setTokenCookie(c, auth.AccessTokenCookieName, accessToken, cookieExp)
- userMessage := convertUserFromStore(user)
- return c.JSON(http.StatusOK, userMessage)
- }
- // SignInSSO godoc
- //
- // @Summary Sign-in to memos using SSO.
- // @Tags auth
- // @Accept json
- // @Produce json
- // @Param body body SSOSignIn true "SSO sign-in object"
- // @Success 200 {object} store.User "User information"
- // @Failure 400 {object} nil "Malformatted signin request"
- // @Failure 401 {object} nil "Access denied, identifier does not match the filter."
- // @Failure 403 {object} nil "User has been archived with username {username}"
- // @Failure 404 {object} nil "Identity provider not found"
- // @Failure 500 {object} nil "Failed to find identity provider | Failed to create identity provider instance | Failed to exchange token | Failed to get user info | Failed to compile identifier filter | Incorrect login credentials, please try again | Failed to generate random password | Failed to generate password hash | Failed to create user | Failed to generate tokens | Failed to create activity"
- // @Router /api/v1/auth/signin/sso [POST]
- func (s *APIV1Service) SignInSSO(c echo.Context) error {
- ctx := c.Request().Context()
- signin := &SSOSignIn{}
- if err := json.NewDecoder(c.Request().Body).Decode(signin); err != nil {
- return echo.NewHTTPError(http.StatusBadRequest, "Malformatted signin request").SetInternal(err)
- }
- identityProvider, err := s.Store.GetIdentityProvider(ctx, &store.FindIdentityProvider{
- ID: &signin.IdentityProviderID,
- })
- if err != nil {
- return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find identity provider").SetInternal(err)
- }
- if identityProvider == nil {
- return echo.NewHTTPError(http.StatusNotFound, "Identity provider not found")
- }
- var userInfo *idp.IdentityProviderUserInfo
- if identityProvider.Type == store.IdentityProviderOAuth2Type {
- oauth2IdentityProvider, err := oauth2.NewIdentityProvider(identityProvider.Config.OAuth2Config)
- if err != nil {
- return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create identity provider instance").SetInternal(err)
- }
- token, err := oauth2IdentityProvider.ExchangeToken(ctx, signin.RedirectURI, signin.Code)
- if err != nil {
- return echo.NewHTTPError(http.StatusInternalServerError, "Failed to exchange token").SetInternal(err)
- }
- userInfo, err = oauth2IdentityProvider.UserInfo(token)
- if err != nil {
- return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get user info").SetInternal(err)
- }
- }
- identifierFilter := identityProvider.IdentifierFilter
- if identifierFilter != "" {
- identifierFilterRegex, err := regexp.Compile(identifierFilter)
- if err != nil {
- return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compile identifier filter").SetInternal(err)
- }
- if !identifierFilterRegex.MatchString(userInfo.Identifier) {
- return echo.NewHTTPError(http.StatusUnauthorized, "Access denied, identifier does not match the filter.").SetInternal(err)
- }
- }
- user, err := s.Store.GetUser(ctx, &store.FindUser{
- Username: &userInfo.Identifier,
- })
- if err != nil {
- return echo.NewHTTPError(http.StatusInternalServerError, "Incorrect login credentials, please try again")
- }
- if user == nil {
- allowSignUpSetting, err := s.Store.GetSystemSetting(ctx, &store.FindSystemSetting{
- Name: SystemSettingAllowSignUpName.String(),
- })
- if err != nil {
- return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting").SetInternal(err)
- }
- allowSignUpSettingValue := false
- if allowSignUpSetting != nil {
- err = json.Unmarshal([]byte(allowSignUpSetting.Value), &allowSignUpSettingValue)
- if err != nil {
- return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal system setting allow signup").SetInternal(err)
- }
- }
- if !allowSignUpSettingValue {
- return echo.NewHTTPError(http.StatusUnauthorized, "signup is disabled").SetInternal(err)
- }
- userCreate := &store.User{
- Username: userInfo.Identifier,
- // The new signup user should be normal user by default.
- Role: store.RoleUser,
- Nickname: userInfo.DisplayName,
- Email: userInfo.Email,
- }
- password, err := util.RandomString(20)
- if err != nil {
- return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate random password").SetInternal(err)
- }
- passwordHash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
- if err != nil {
- return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate password hash").SetInternal(err)
- }
- userCreate.PasswordHash = string(passwordHash)
- user, err = s.Store.CreateUser(ctx, userCreate)
- if err != nil {
- return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create user").SetInternal(err)
- }
- }
- if user.RowStatus == store.Archived {
- return echo.NewHTTPError(http.StatusForbidden, fmt.Sprintf("User has been archived with username %s", userInfo.Identifier))
- }
- accessToken, err := auth.GenerateAccessToken(user.Username, user.ID, time.Now().Add(auth.AccessTokenDuration), []byte(s.Secret))
- if err != nil {
- return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to generate tokens, err: %s", err)).SetInternal(err)
- }
- if err := s.UpsertAccessTokenToStore(ctx, user, accessToken); err != nil {
- return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to upsert access token, err: %s", err)).SetInternal(err)
- }
- cookieExp := time.Now().Add(auth.CookieExpDuration)
- setTokenCookie(c, auth.AccessTokenCookieName, accessToken, cookieExp)
- userMessage := convertUserFromStore(user)
- return c.JSON(http.StatusOK, userMessage)
- }
- // SignOut godoc
- //
- // @Summary Sign-out from memos.
- // @Tags auth
- // @Produce json
- // @Success 200 {boolean} true "Sign-out success"
- // @Router /api/v1/auth/signout [POST]
- func (s *APIV1Service) SignOut(c echo.Context) error {
- ctx := c.Request().Context()
- accessToken := findAccessToken(c)
- userID, _ := getUserIDFromAccessToken(accessToken, s.Secret)
- userAccessTokens, err := s.Store.GetUserAccessTokens(ctx, userID)
- // Auto remove the current access token from the user access tokens.
- if err == nil && len(userAccessTokens) != 0 {
- accessTokens := []*storepb.AccessTokensUserSetting_AccessToken{}
- for _, userAccessToken := range userAccessTokens {
- if accessToken != userAccessToken.AccessToken {
- accessTokens = append(accessTokens, userAccessToken)
- }
- }
- if _, err := s.Store.UpsertUserSettingV1(ctx, &storepb.UserSetting{
- UserId: userID,
- Key: storepb.UserSettingKey_USER_SETTING_ACCESS_TOKENS,
- Value: &storepb.UserSetting_AccessTokens{
- AccessTokens: &storepb.AccessTokensUserSetting{
- AccessTokens: accessTokens,
- },
- },
- }); err != nil {
- return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to upsert user setting, err: %s", err)).SetInternal(err)
- }
- }
- removeAccessTokenAndCookies(c)
- return c.JSON(http.StatusOK, true)
- }
- // SignUp godoc
- //
- // @Summary Sign-up to memos.
- // @Tags auth
- // @Accept json
- // @Produce json
- // @Param body body SignUp true "Sign-up object"
- // @Success 200 {object} store.User "User information"
- // @Failure 400 {object} nil "Malformatted signup request | Failed to find users"
- // @Failure 401 {object} nil "signup is disabled"
- // @Failure 403 {object} nil "Forbidden"
- // @Failure 404 {object} nil "Not found"
- // @Failure 500 {object} nil "Failed to find system setting | Failed to unmarshal system setting allow signup | Failed to generate password hash | Failed to create user | Failed to generate tokens | Failed to create activity"
- // @Router /api/v1/auth/signup [POST]
- func (s *APIV1Service) SignUp(c echo.Context) error {
- ctx := c.Request().Context()
- signup := &SignUp{}
- if err := json.NewDecoder(c.Request().Body).Decode(signup); err != nil {
- return echo.NewHTTPError(http.StatusBadRequest, "Malformatted signup request").SetInternal(err)
- }
- hostUserType := store.RoleHost
- existedHostUsers, err := s.Store.ListUsers(ctx, &store.FindUser{
- Role: &hostUserType,
- })
- if err != nil {
- return echo.NewHTTPError(http.StatusBadRequest, "Failed to find users").SetInternal(err)
- }
- if !usernameMatcher.MatchString(strings.ToLower(signup.Username)) {
- return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid username %s", signup.Username)).SetInternal(err)
- }
- userCreate := &store.User{
- Username: signup.Username,
- // The new signup user should be normal user by default.
- Role: store.RoleUser,
- Nickname: signup.Username,
- }
- if len(existedHostUsers) == 0 {
- // Change the default role to host if there is no host user.
- userCreate.Role = store.RoleHost
- } else {
- allowSignUpSetting, err := s.Store.GetSystemSetting(ctx, &store.FindSystemSetting{
- Name: SystemSettingAllowSignUpName.String(),
- })
- if err != nil {
- return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting").SetInternal(err)
- }
- allowSignUpSettingValue := false
- if allowSignUpSetting != nil {
- err = json.Unmarshal([]byte(allowSignUpSetting.Value), &allowSignUpSettingValue)
- if err != nil {
- return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal system setting allow signup").SetInternal(err)
- }
- }
- if !allowSignUpSettingValue {
- return echo.NewHTTPError(http.StatusUnauthorized, "signup is disabled").SetInternal(err)
- }
- }
- passwordHash, err := bcrypt.GenerateFromPassword([]byte(signup.Password), bcrypt.DefaultCost)
- if err != nil {
- return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate password hash").SetInternal(err)
- }
- userCreate.PasswordHash = string(passwordHash)
- user, err := s.Store.CreateUser(ctx, userCreate)
- if err != nil {
- return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create user").SetInternal(err)
- }
- accessToken, err := auth.GenerateAccessToken(user.Username, user.ID, time.Now().Add(auth.AccessTokenDuration), []byte(s.Secret))
- if err != nil {
- return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to generate tokens, err: %s", err)).SetInternal(err)
- }
- if err := s.UpsertAccessTokenToStore(ctx, user, accessToken); err != nil {
- return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to upsert access token, err: %s", err)).SetInternal(err)
- }
- cookieExp := time.Now().Add(auth.CookieExpDuration)
- setTokenCookie(c, auth.AccessTokenCookieName, accessToken, cookieExp)
- userMessage := convertUserFromStore(user)
- return c.JSON(http.StatusOK, userMessage)
- }
- func (s *APIV1Service) UpsertAccessTokenToStore(ctx context.Context, user *store.User, accessToken string) error {
- userAccessTokens, err := s.Store.GetUserAccessTokens(ctx, user.ID)
- if err != nil {
- return errors.Wrap(err, "failed to get user access tokens")
- }
- userAccessToken := storepb.AccessTokensUserSetting_AccessToken{
- AccessToken: accessToken,
- Description: "Account sign in",
- }
- userAccessTokens = append(userAccessTokens, &userAccessToken)
- if _, err := s.Store.UpsertUserSettingV1(ctx, &storepb.UserSetting{
- UserId: user.ID,
- Key: storepb.UserSettingKey_USER_SETTING_ACCESS_TOKENS,
- Value: &storepb.UserSetting_AccessTokens{
- AccessTokens: &storepb.AccessTokensUserSetting{
- AccessTokens: userAccessTokens,
- },
- },
- }); err != nil {
- return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to upsert user setting, err: %s", err)).SetInternal(err)
- }
- return nil
- }
- // removeAccessTokenAndCookies removes the jwt token from the cookies.
- func removeAccessTokenAndCookies(c echo.Context) {
- cookieExp := time.Now().Add(-1 * time.Hour)
- setTokenCookie(c, auth.AccessTokenCookieName, "", cookieExp)
- }
- // setTokenCookie sets the token to the cookie.
- func setTokenCookie(c echo.Context, name, token string, expiration time.Time) {
- cookie := new(http.Cookie)
- cookie.Name = name
- cookie.Value = token
- cookie.Expires = expiration
- cookie.Path = "/"
- // Http-only helps mitigate the risk of client side script accessing the protected cookie.
- cookie.HttpOnly = true
- cookie.SameSite = http.SameSiteStrictMode
- c.SetCookie(cookie)
- }
|