auth.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. package v1
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "net/http"
  7. "regexp"
  8. "strings"
  9. "time"
  10. "github.com/labstack/echo/v4"
  11. "github.com/pkg/errors"
  12. "golang.org/x/crypto/bcrypt"
  13. "github.com/usememos/memos/api/auth"
  14. "github.com/usememos/memos/internal/util"
  15. "github.com/usememos/memos/plugin/idp"
  16. "github.com/usememos/memos/plugin/idp/oauth2"
  17. storepb "github.com/usememos/memos/proto/gen/store"
  18. "github.com/usememos/memos/store"
  19. )
  20. type SignIn struct {
  21. Username string `json:"username"`
  22. Password string `json:"password"`
  23. Remember bool `json:"remember"`
  24. }
  25. type SSOSignIn struct {
  26. IdentityProviderID int32 `json:"identityProviderId"`
  27. Code string `json:"code"`
  28. RedirectURI string `json:"redirectUri"`
  29. }
  30. type SignUp struct {
  31. Username string `json:"username"`
  32. Password string `json:"password"`
  33. }
  34. func (s *APIV1Service) registerAuthRoutes(g *echo.Group) {
  35. g.POST("/auth/signin", s.SignIn)
  36. g.POST("/auth/signin/sso", s.SignInSSO)
  37. g.POST("/auth/signout", s.SignOut)
  38. g.POST("/auth/signup", s.SignUp)
  39. }
  40. // SignIn godoc
  41. //
  42. // @Summary Sign-in to memos.
  43. // @Tags auth
  44. // @Accept json
  45. // @Produce json
  46. // @Param body body SignIn true "Sign-in object"
  47. // @Success 200 {object} store.User "User information"
  48. // @Failure 400 {object} nil "Malformatted signin request"
  49. // @Failure 401 {object} nil "Password login is deactivated | Incorrect login credentials, please try again"
  50. // @Failure 403 {object} nil "User has been archived with username %s"
  51. // @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"
  52. // @Router /api/v1/auth/signin [POST]
  53. func (s *APIV1Service) SignIn(c echo.Context) error {
  54. ctx := c.Request().Context()
  55. workspaceGeneralSetting, err := s.Store.GetWorkspaceGeneralSetting(ctx)
  56. if err != nil {
  57. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting").SetInternal(err)
  58. }
  59. if workspaceGeneralSetting.DisallowPasswordLogin {
  60. return echo.NewHTTPError(http.StatusUnauthorized, "password login is deactivated").SetInternal(err)
  61. }
  62. signin := &SignIn{}
  63. if err := json.NewDecoder(c.Request().Body).Decode(signin); err != nil {
  64. return echo.NewHTTPError(http.StatusBadRequest, "Malformatted signin request").SetInternal(err)
  65. }
  66. user, err := s.Store.GetUser(ctx, &store.FindUser{
  67. Username: &signin.Username,
  68. })
  69. if err != nil {
  70. return echo.NewHTTPError(http.StatusInternalServerError, "Incorrect login credentials, please try again")
  71. }
  72. if user == nil {
  73. return echo.NewHTTPError(http.StatusUnauthorized, "Incorrect login credentials, please try again")
  74. } else if user.RowStatus == store.Archived {
  75. return echo.NewHTTPError(http.StatusForbidden, fmt.Sprintf("User has been archived with username %s", signin.Username))
  76. }
  77. // Compare the stored hashed password, with the hashed version of the password that was received.
  78. if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(signin.Password)); err != nil {
  79. // If the two passwords don't match, return a 401 status.
  80. return echo.NewHTTPError(http.StatusUnauthorized, "Incorrect login credentials, please try again")
  81. }
  82. var expireAt time.Time
  83. // Set cookie expiration to 100 years to make it persistent.
  84. cookieExp := time.Now().AddDate(100, 0, 0)
  85. if !signin.Remember {
  86. expireAt = time.Now().Add(auth.AccessTokenDuration)
  87. cookieExp = time.Now().Add(auth.CookieExpDuration)
  88. }
  89. accessToken, err := auth.GenerateAccessToken(user.Username, user.ID, expireAt, []byte(s.Secret))
  90. if err != nil {
  91. return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to generate tokens, err: %s", err)).SetInternal(err)
  92. }
  93. if err := s.UpsertAccessTokenToStore(ctx, user, accessToken); err != nil {
  94. return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to upsert access token, err: %s", err)).SetInternal(err)
  95. }
  96. setTokenCookie(c, auth.AccessTokenCookieName, accessToken, cookieExp)
  97. userMessage := convertUserFromStore(user)
  98. return c.JSON(http.StatusOK, userMessage)
  99. }
  100. // SignInSSO godoc
  101. //
  102. // @Summary Sign-in to memos using SSO.
  103. // @Tags auth
  104. // @Accept json
  105. // @Produce json
  106. // @Param body body SSOSignIn true "SSO sign-in object"
  107. // @Success 200 {object} store.User "User information"
  108. // @Failure 400 {object} nil "Malformatted signin request"
  109. // @Failure 401 {object} nil "Access denied, identifier does not match the filter."
  110. // @Failure 403 {object} nil "User has been archived with username {username}"
  111. // @Failure 404 {object} nil "Identity provider not found"
  112. // @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"
  113. // @Router /api/v1/auth/signin/sso [POST]
  114. func (s *APIV1Service) SignInSSO(c echo.Context) error {
  115. ctx := c.Request().Context()
  116. signin := &SSOSignIn{}
  117. if err := json.NewDecoder(c.Request().Body).Decode(signin); err != nil {
  118. return echo.NewHTTPError(http.StatusBadRequest, "Malformatted signin request").SetInternal(err)
  119. }
  120. identityProvider, err := s.Store.GetIdentityProvider(ctx, &store.FindIdentityProvider{
  121. ID: &signin.IdentityProviderID,
  122. })
  123. if err != nil {
  124. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find identity provider").SetInternal(err)
  125. }
  126. if identityProvider == nil {
  127. return echo.NewHTTPError(http.StatusNotFound, "Identity provider not found")
  128. }
  129. var userInfo *idp.IdentityProviderUserInfo
  130. if identityProvider.Type == store.IdentityProviderOAuth2Type {
  131. oauth2IdentityProvider, err := oauth2.NewIdentityProvider(identityProvider.Config.OAuth2Config)
  132. if err != nil {
  133. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create identity provider instance").SetInternal(err)
  134. }
  135. token, err := oauth2IdentityProvider.ExchangeToken(ctx, signin.RedirectURI, signin.Code)
  136. if err != nil {
  137. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to exchange token").SetInternal(err)
  138. }
  139. userInfo, err = oauth2IdentityProvider.UserInfo(token)
  140. if err != nil {
  141. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get user info").SetInternal(err)
  142. }
  143. }
  144. identifierFilter := identityProvider.IdentifierFilter
  145. if identifierFilter != "" {
  146. identifierFilterRegex, err := regexp.Compile(identifierFilter)
  147. if err != nil {
  148. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compile identifier filter").SetInternal(err)
  149. }
  150. if !identifierFilterRegex.MatchString(userInfo.Identifier) {
  151. return echo.NewHTTPError(http.StatusUnauthorized, "Access denied, identifier does not match the filter.").SetInternal(err)
  152. }
  153. }
  154. user, err := s.Store.GetUser(ctx, &store.FindUser{
  155. Username: &userInfo.Identifier,
  156. })
  157. if err != nil {
  158. return echo.NewHTTPError(http.StatusInternalServerError, "Incorrect login credentials, please try again")
  159. }
  160. if user == nil {
  161. workspaceGeneralSetting, err := s.Store.GetWorkspaceGeneralSetting(ctx)
  162. if err != nil {
  163. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting").SetInternal(err)
  164. }
  165. if workspaceGeneralSetting.DisallowSignup {
  166. return echo.NewHTTPError(http.StatusUnauthorized, "signup is disabled").SetInternal(err)
  167. }
  168. userCreate := &store.User{
  169. Username: userInfo.Identifier,
  170. // The new signup user should be normal user by default.
  171. Role: store.RoleUser,
  172. Nickname: userInfo.DisplayName,
  173. Email: userInfo.Email,
  174. }
  175. password, err := util.RandomString(20)
  176. if err != nil {
  177. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate random password").SetInternal(err)
  178. }
  179. passwordHash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
  180. if err != nil {
  181. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate password hash").SetInternal(err)
  182. }
  183. userCreate.PasswordHash = string(passwordHash)
  184. user, err = s.Store.CreateUser(ctx, userCreate)
  185. if err != nil {
  186. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create user").SetInternal(err)
  187. }
  188. }
  189. if user.RowStatus == store.Archived {
  190. return echo.NewHTTPError(http.StatusForbidden, fmt.Sprintf("User has been archived with username %s", userInfo.Identifier))
  191. }
  192. accessToken, err := auth.GenerateAccessToken(user.Username, user.ID, time.Now().Add(auth.AccessTokenDuration), []byte(s.Secret))
  193. if err != nil {
  194. return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to generate tokens, err: %s", err)).SetInternal(err)
  195. }
  196. if err := s.UpsertAccessTokenToStore(ctx, user, accessToken); err != nil {
  197. return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to upsert access token, err: %s", err)).SetInternal(err)
  198. }
  199. cookieExp := time.Now().Add(auth.CookieExpDuration)
  200. setTokenCookie(c, auth.AccessTokenCookieName, accessToken, cookieExp)
  201. userMessage := convertUserFromStore(user)
  202. return c.JSON(http.StatusOK, userMessage)
  203. }
  204. // SignOut godoc
  205. //
  206. // @Summary Sign-out from memos.
  207. // @Tags auth
  208. // @Produce json
  209. // @Success 200 {boolean} true "Sign-out success"
  210. // @Router /api/v1/auth/signout [POST]
  211. func (s *APIV1Service) SignOut(c echo.Context) error {
  212. accessToken := findAccessToken(c)
  213. userID, _ := getUserIDFromAccessToken(accessToken, s.Secret)
  214. err := removeAccessTokenAndCookies(c, s.Store, userID, accessToken)
  215. if err != nil {
  216. return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to remove access token, err: %s", err)).SetInternal(err)
  217. }
  218. return c.JSON(http.StatusOK, true)
  219. }
  220. // SignUp godoc
  221. //
  222. // @Summary Sign-up to memos.
  223. // @Tags auth
  224. // @Accept json
  225. // @Produce json
  226. // @Param body body SignUp true "Sign-up object"
  227. // @Success 200 {object} store.User "User information"
  228. // @Failure 400 {object} nil "Malformatted signup request | Failed to find users"
  229. // @Failure 401 {object} nil "signup is disabled"
  230. // @Failure 403 {object} nil "Forbidden"
  231. // @Failure 404 {object} nil "Not found"
  232. // @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"
  233. // @Router /api/v1/auth/signup [POST]
  234. func (s *APIV1Service) SignUp(c echo.Context) error {
  235. ctx := c.Request().Context()
  236. signup := &SignUp{}
  237. if err := json.NewDecoder(c.Request().Body).Decode(signup); err != nil {
  238. return echo.NewHTTPError(http.StatusBadRequest, "Malformatted signup request").SetInternal(err)
  239. }
  240. hostUserType := store.RoleHost
  241. existedHostUsers, err := s.Store.ListUsers(ctx, &store.FindUser{
  242. Role: &hostUserType,
  243. })
  244. if err != nil {
  245. return echo.NewHTTPError(http.StatusBadRequest, "Failed to find users").SetInternal(err)
  246. }
  247. if !util.ResourceNameMatcher.MatchString(strings.ToLower(signup.Username)) {
  248. return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid username %s", signup.Username)).SetInternal(err)
  249. }
  250. userCreate := &store.User{
  251. Username: signup.Username,
  252. // The new signup user should be normal user by default.
  253. Role: store.RoleUser,
  254. Nickname: signup.Username,
  255. }
  256. if len(existedHostUsers) == 0 {
  257. // Change the default role to host if there is no host user.
  258. userCreate.Role = store.RoleHost
  259. } else {
  260. workspaceGeneralSetting, err := s.Store.GetWorkspaceGeneralSetting(ctx)
  261. if err != nil {
  262. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting").SetInternal(err)
  263. }
  264. if workspaceGeneralSetting.DisallowSignup {
  265. return echo.NewHTTPError(http.StatusUnauthorized, "signup is disabled").SetInternal(err)
  266. }
  267. if workspaceGeneralSetting.DisallowPasswordLogin {
  268. return echo.NewHTTPError(http.StatusUnauthorized, "password login is deactivated").SetInternal(err)
  269. }
  270. }
  271. passwordHash, err := bcrypt.GenerateFromPassword([]byte(signup.Password), bcrypt.DefaultCost)
  272. if err != nil {
  273. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate password hash").SetInternal(err)
  274. }
  275. userCreate.PasswordHash = string(passwordHash)
  276. user, err := s.Store.CreateUser(ctx, userCreate)
  277. if err != nil {
  278. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create user").SetInternal(err)
  279. }
  280. accessToken, err := auth.GenerateAccessToken(user.Username, user.ID, time.Now().Add(auth.AccessTokenDuration), []byte(s.Secret))
  281. if err != nil {
  282. return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to generate tokens, err: %s", err)).SetInternal(err)
  283. }
  284. if err := s.UpsertAccessTokenToStore(ctx, user, accessToken); err != nil {
  285. return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to upsert access token, err: %s", err)).SetInternal(err)
  286. }
  287. cookieExp := time.Now().Add(auth.CookieExpDuration)
  288. setTokenCookie(c, auth.AccessTokenCookieName, accessToken, cookieExp)
  289. userMessage := convertUserFromStore(user)
  290. return c.JSON(http.StatusOK, userMessage)
  291. }
  292. func (s *APIV1Service) UpsertAccessTokenToStore(ctx context.Context, user *store.User, accessToken string) error {
  293. userAccessTokens, err := s.Store.GetUserAccessTokens(ctx, user.ID)
  294. if err != nil {
  295. return errors.Wrap(err, "failed to get user access tokens")
  296. }
  297. userAccessToken := storepb.AccessTokensUserSetting_AccessToken{
  298. AccessToken: accessToken,
  299. Description: "Account sign in",
  300. }
  301. userAccessTokens = append(userAccessTokens, &userAccessToken)
  302. if _, err := s.Store.UpsertUserSetting(ctx, &storepb.UserSetting{
  303. UserId: user.ID,
  304. Key: storepb.UserSettingKey_USER_SETTING_ACCESS_TOKENS,
  305. Value: &storepb.UserSetting_AccessTokens{
  306. AccessTokens: &storepb.AccessTokensUserSetting{
  307. AccessTokens: userAccessTokens,
  308. },
  309. },
  310. }); err != nil {
  311. return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to upsert user setting, err: %s", err)).SetInternal(err)
  312. }
  313. return nil
  314. }
  315. // removeAccessTokenAndCookies removes the jwt token from the cookies.
  316. func removeAccessTokenAndCookies(c echo.Context, s *store.Store, userID int32, token string) error {
  317. err := s.RemoveUserAccessToken(c.Request().Context(), userID, token)
  318. if err != nil {
  319. return err
  320. }
  321. cookieExp := time.Now().Add(-1 * time.Hour)
  322. setTokenCookie(c, auth.AccessTokenCookieName, "", cookieExp)
  323. return nil
  324. }
  325. // setTokenCookie sets the token to the cookie.
  326. func setTokenCookie(c echo.Context, name, token string, expiration time.Time) {
  327. cookie := new(http.Cookie)
  328. cookie.Name = name
  329. cookie.Value = token
  330. cookie.Expires = expiration
  331. cookie.Path = "/"
  332. // Http-only helps mitigate the risk of client side script accessing the protected cookie.
  333. cookie.HttpOnly = true
  334. cookie.SameSite = http.SameSiteStrictMode
  335. c.SetCookie(cookie)
  336. }