2
0

handler.go 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. package authn
  2. import (
  3. "context"
  4. "fmt"
  5. "net/http"
  6. "strings"
  7. "github.com/gorilla/sessions"
  8. "github.com/porter-dev/porter/api/server/shared/apierrors"
  9. "github.com/porter-dev/porter/api/server/shared/config"
  10. "github.com/porter-dev/porter/api/types"
  11. "github.com/porter-dev/porter/internal/auth/token"
  12. "github.com/porter-dev/porter/internal/models"
  13. )
  14. // AuthNFactory generates a middleware handler `AuthN`
  15. type AuthNFactory struct {
  16. config *config.Config
  17. }
  18. // NewAuthNFactory returns an `AuthNFactory` that uses the passed-in server
  19. // config
  20. func NewAuthNFactory(
  21. config *config.Config,
  22. ) *AuthNFactory {
  23. return &AuthNFactory{config}
  24. }
  25. // NewAuthenticated creates a new instance of `AuthN` that implements the http.Handler
  26. // interface.
  27. func (f *AuthNFactory) NewAuthenticated(next http.Handler) http.Handler {
  28. return &AuthN{next, f.config, false}
  29. }
  30. // NewAuthenticatedWithRedirect creates a new instance of `AuthN` that implements the http.Handler
  31. // interface. This handler redirects the user to login if the user is not attached, and stores a
  32. // redirect URI in the session, if the session exists.
  33. func (f *AuthNFactory) NewAuthenticatedWithRedirect(next http.Handler) http.Handler {
  34. return &AuthN{next, f.config, true}
  35. }
  36. // AuthN implements the authentication middleware
  37. type AuthN struct {
  38. next http.Handler
  39. config *config.Config
  40. redirect bool
  41. }
  42. // ServeHTTP attaches an authenticated subject to the request context,
  43. // or serves a forbidden error. If authenticated, it calls the next handler.
  44. //
  45. // A token can either be issued for a specific project id or for a user. In the case
  46. // of a project id, we attach a service account to the context. In the case of a
  47. // user, we attach that user to the context.
  48. func (authn *AuthN) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  49. // first check for a bearer token
  50. tok, err := authn.getTokenFromRequest(r)
  51. // if the error is not an invalid auth error, the token was invalid, and we throw error
  52. // forbidden. If the error was an invalid auth error, we look for a cookie.
  53. if err != nil && err != errInvalidAuthHeader {
  54. authn.sendForbiddenError(err, w, r)
  55. return
  56. } else if err == nil && tok != nil {
  57. authn.verifyTokenWithNext(w, r, tok)
  58. return
  59. }
  60. // if the bearer token is not found, look for a request cookie
  61. session, err := authn.config.Store.Get(r, authn.config.ServerConf.CookieName)
  62. if err != nil {
  63. session.Values["authenticated"] = false
  64. // we attempt to save the session, but do not catch the error since we send the
  65. // forbidden error regardless
  66. session.Save(r, w)
  67. authn.sendForbiddenError(err, w, r)
  68. return
  69. }
  70. if auth, ok := session.Values["authenticated"].(bool); !auth || !ok {
  71. authn.handleForbiddenForSession(w, r, fmt.Errorf("stored cookie was not authenticated"), session)
  72. return
  73. }
  74. // read the user id in the token
  75. userID, ok := session.Values["user_id"].(uint)
  76. if !ok {
  77. authn.handleForbiddenForSession(w, r, fmt.Errorf("could not cast user_id to uint"), session)
  78. return
  79. }
  80. authn.nextWithUserID(w, r, userID)
  81. }
  82. func (authn *AuthN) handleForbiddenForSession(
  83. w http.ResponseWriter,
  84. r *http.Request,
  85. err error,
  86. session *sessions.Session,
  87. ) {
  88. if authn.redirect {
  89. // need state parameter to validate when redirected
  90. if r.URL.RawQuery == "" {
  91. session.Values["redirect_uri"] = r.URL.Path
  92. } else {
  93. session.Values["redirect_uri"] = r.URL.Path + "?" + r.URL.RawQuery
  94. }
  95. session.Save(r, w)
  96. // special logic for GET /api/projects/{project_id}/invites/{token}
  97. if r.Method == "GET" && strings.Contains(r.URL.Path, "/invites/") &&
  98. !strings.HasSuffix(r.URL.Path, "/invites/") {
  99. pathSegments := strings.Split(r.URL.Path, "/")
  100. inviteToken := pathSegments[len(pathSegments)-1]
  101. invite, err := authn.config.Repo.Invite().ReadInviteByToken(inviteToken)
  102. if err != nil || invite.ProjectID == 0 || invite.Email == "" {
  103. apierrors.HandleAPIError(authn.config.Logger, authn.config.Alerter, w, r,
  104. apierrors.NewErrPassThroughToClient(fmt.Errorf("invalid invite token"), http.StatusBadRequest), true)
  105. return
  106. }
  107. if invite.IsExpired() || invite.IsAccepted() {
  108. apierrors.HandleAPIError(authn.config.Logger, authn.config.Alerter, w, r,
  109. apierrors.NewErrPassThroughToClient(fmt.Errorf("invite has expired"), http.StatusBadRequest), true)
  110. return
  111. }
  112. http.Redirect(w, r, "/register?email="+invite.Email, http.StatusTemporaryRedirect)
  113. return
  114. }
  115. http.Redirect(w, r, "/dashboard", http.StatusFound)
  116. } else {
  117. authn.sendForbiddenError(err, w, r)
  118. }
  119. }
  120. func (authn *AuthN) verifyTokenWithNext(w http.ResponseWriter, r *http.Request, tok *token.Token) {
  121. // if the token has a stored token id and secret we check that the token is valid in the database
  122. if tok.Secret != "" && tok.TokenID != "" {
  123. apiToken, err := authn.config.Repo.APIToken().ReadAPIToken(tok.ProjectID, tok.TokenID)
  124. if err != nil {
  125. authn.sendForbiddenError(fmt.Errorf("token with id %s not valid", tok.TokenID), w, r)
  126. return
  127. }
  128. // first ensure that the token hasn't been revoked, and the token has not expired
  129. if apiToken.Revoked || apiToken.IsExpired() {
  130. authn.sendForbiddenError(fmt.Errorf("token with id %s not valid", tok.TokenID), w, r)
  131. return
  132. }
  133. authn.nextWithAPIToken(w, r, apiToken)
  134. } else {
  135. // otherwise we just use nextWithUser using the `iby` field for the token
  136. authn.nextWithUserID(w, r, tok.IBy)
  137. }
  138. }
  139. // nextWithAPIToken sets the token in context
  140. func (authn *AuthN) nextWithAPIToken(w http.ResponseWriter, r *http.Request, tok *models.APIToken) {
  141. ctx := r.Context()
  142. ctx = context.WithValue(ctx, "api_token", tok)
  143. // add a service account user to the project: note that any calls depending on a DB lookup for the
  144. // user will fail
  145. ctx = context.WithValue(ctx, types.UserScope, &models.User{
  146. Email: fmt.Sprintf("%s-%d", tok.Name, tok.ProjectID),
  147. EmailVerified: true,
  148. })
  149. r = r.Clone(ctx)
  150. authn.next.ServeHTTP(w, r)
  151. }
  152. // nextWithUserID calls the next handler with the user set in the context with key
  153. // `types.UserScope`.
  154. func (authn *AuthN) nextWithUserID(w http.ResponseWriter, r *http.Request, userID uint) {
  155. // search for the user
  156. user, err := authn.config.Repo.User().ReadUser(userID)
  157. if err != nil {
  158. authn.sendForbiddenError(fmt.Errorf("user with id %d not found in database", userID), w, r)
  159. return
  160. }
  161. // add the user to the context
  162. ctx := r.Context()
  163. ctx = context.WithValue(ctx, types.UserScope, user)
  164. r = r.Clone(ctx)
  165. authn.next.ServeHTTP(w, r)
  166. }
  167. // sendForbiddenError sends a 403 Forbidden error to the end user while logging a
  168. // specific error
  169. func (authn *AuthN) sendForbiddenError(err error, w http.ResponseWriter, r *http.Request) {
  170. reqErr := apierrors.NewErrForbidden(err)
  171. apierrors.HandleAPIError(authn.config.Logger, authn.config.Alerter, w, r, reqErr, true)
  172. }
  173. var (
  174. errInvalidToken = fmt.Errorf("authorization header exists, but token is not valid")
  175. errInvalidAuthHeader = fmt.Errorf("invalid authorization header in request")
  176. )
  177. // getTokenFromRequest finds an `Authorization` header of the form `Bearer <token>`,
  178. // and returns a valid token if it exists.
  179. func (authn *AuthN) getTokenFromRequest(r *http.Request) (*token.Token, error) {
  180. reqToken := r.Header.Get("Authorization")
  181. splitToken := strings.Split(reqToken, "Bearer")
  182. if len(splitToken) != 2 {
  183. return nil, errInvalidAuthHeader
  184. }
  185. reqToken = strings.TrimSpace(splitToken[1])
  186. tok, err := token.GetTokenFromEncoded(reqToken, authn.config.TokenConf)
  187. if err != nil {
  188. return nil, errInvalidToken
  189. }
  190. return tok, nil
  191. }