handler.go 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. package authn
  2. import (
  3. "context"
  4. "fmt"
  5. "net/http"
  6. "net/url"
  7. "strings"
  8. "time"
  9. "github.com/gorilla/sessions"
  10. "github.com/porter-dev/porter/api/server/shared/apierrors"
  11. "github.com/porter-dev/porter/api/server/shared/config"
  12. "github.com/porter-dev/porter/api/types"
  13. "github.com/porter-dev/porter/internal/auth/token"
  14. "github.com/porter-dev/porter/internal/models"
  15. )
  16. // AuthNFactory generates a middleware handler `AuthN`
  17. type AuthNFactory struct {
  18. config *config.Config
  19. }
  20. // NewAuthNFactory returns an `AuthNFactory` that uses the passed-in server
  21. // config
  22. func NewAuthNFactory(
  23. config *config.Config,
  24. ) *AuthNFactory {
  25. return &AuthNFactory{config}
  26. }
  27. // NewAuthenticated creates a new instance of `AuthN` that implements the http.Handler
  28. // interface.
  29. func (f *AuthNFactory) NewAuthenticated(next http.Handler) http.Handler {
  30. return &AuthN{next, f.config, false}
  31. }
  32. // NewAuthenticatedWithRedirect creates a new instance of `AuthN` that implements the http.Handler
  33. // interface. This handler redirects the user to login if the user is not attached, and stores a
  34. // redirect URI in the session, if the session exists.
  35. func (f *AuthNFactory) NewAuthenticatedWithRedirect(next http.Handler) http.Handler {
  36. return &AuthN{next, f.config, true}
  37. }
  38. // AuthN implements the authentication middleware
  39. type AuthN struct {
  40. next http.Handler
  41. config *config.Config
  42. redirect bool
  43. }
  44. // ServeHTTP attaches an authenticated subject to the request context,
  45. // or serves a forbidden error. If authenticated, it calls the next handler.
  46. //
  47. // A token can either be issued for a specific project id or for a user. In the case
  48. // of a project id, we attach a service account to the context. In the case of a
  49. // user, we attach that user to the context.
  50. func (authn *AuthN) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  51. // first check for a bearer token
  52. tok, err := authn.getTokenFromRequest(r)
  53. // if the error is not an invalid auth error, the token was invalid, and we throw error
  54. // forbidden. If the error was an invalid auth error, we look for a cookie.
  55. if err != nil && err != errInvalidAuthHeader {
  56. authn.sendForbiddenError(err, w, r)
  57. return
  58. } else if err == nil && tok != nil {
  59. authn.verifyTokenWithNext(w, r, tok)
  60. return
  61. }
  62. // if the bearer token is not found, look for a request cookie
  63. // first look for new ory cookie
  64. // set the cookies on the ory client
  65. var cookies string
  66. // this example passes all request.Cookies
  67. // to `ToSession` function
  68. //
  69. // However, you can pass only the value of
  70. // ory_session_projectid cookie to the endpoint
  71. cookies = r.Header.Get("Cookie")
  72. fmt.Println("Cookies: ", cookies)
  73. // check if we have a session
  74. orySession, _, err := authn.config.Ory.FrontendAPI.ToSession(r.Context()).Cookie(cookies).Execute()
  75. if err != nil {
  76. fmt.Println("Ory error: ", err.Error())
  77. }
  78. if err == nil && orySession != nil && *orySession.Active {
  79. fmt.Println("in here")
  80. // get Ory user id
  81. if orySession.Identity != nil {
  82. fmt.Println("now in here")
  83. // get user id from Ory
  84. externalId := orySession.Identity.Id
  85. user, err := authn.config.Repo.User().ReadUserByAuthProvider("ory", externalId)
  86. if err != nil || user == nil {
  87. err := fmt.Errorf("ory user not found in database", externalId)
  88. authn.sendForbiddenError(err, w, r)
  89. return
  90. }
  91. fmt.Println("going next")
  92. authn.nextWithUserID(w, r, user.ID)
  93. return
  94. }
  95. }
  96. // then look for existing porter cookie
  97. session, err := authn.config.Store.Get(r, authn.config.ServerConf.CookieName)
  98. if err != nil {
  99. session.Values["authenticated"] = false
  100. // we attempt to save the session, but do not catch the error since we send the
  101. // forbidden error regardless
  102. session.Save(r, w)
  103. authn.sendForbiddenError(err, w, r)
  104. return
  105. }
  106. cancelTokens := func(lastIssueTime time.Time, cancelEmail string, authn *AuthN, session *sessions.Session) bool {
  107. if email, ok := session.Values["email"]; ok {
  108. if email.(string) == cancelEmail {
  109. timeAsUTC := lastIssueTime.UTC()
  110. sess, _ := authn.config.Repo.Session().SelectSession(&models.Session{Key: session.ID})
  111. if sess.CreatedAt.UTC().Before(timeAsUTC) {
  112. _, _ = authn.config.Repo.Session().DeleteSession(sess)
  113. return true
  114. }
  115. }
  116. }
  117. return false
  118. }
  119. est, err := time.LoadLocation("EST")
  120. // if err == nil {
  121. // authn.handleForbiddenForSession(w, r, fmt.Errorf("error, contact admin"), session)
  122. // return
  123. // }
  124. // TODO: handle error from time.LoadLocation
  125. if err == nil {
  126. if cancelTokens(time.Date(2024, 0o1, 16, 18, 35, 0, 0, est), "support@porter.run", authn, session) {
  127. authn.handleForbiddenForSession(w, r, fmt.Errorf("error, contact admin"), session)
  128. return
  129. }
  130. if cancelTokens(time.Date(2024, 0o1, 16, 18, 35, 0, 0, est), "admin@porter.run", authn, session) {
  131. authn.handleForbiddenForSession(w, r, fmt.Errorf("error, contact admin"), session)
  132. return
  133. }
  134. }
  135. if auth, ok := session.Values["authenticated"].(bool); !auth || !ok {
  136. authn.handleForbiddenForSession(w, r, fmt.Errorf("stored cookie was not authenticated"), session)
  137. return
  138. }
  139. // read the user id in the token
  140. userID, ok := session.Values["user_id"].(uint)
  141. if !ok {
  142. authn.handleForbiddenForSession(w, r, fmt.Errorf("could not cast user_id to uint"), session)
  143. return
  144. }
  145. authn.nextWithUserID(w, r, userID)
  146. }
  147. func (authn *AuthN) handleForbiddenForSession(
  148. w http.ResponseWriter,
  149. r *http.Request,
  150. err error,
  151. session *sessions.Session,
  152. ) {
  153. if authn.redirect {
  154. // need state parameter to validate when redirected
  155. if r.URL.RawQuery == "" {
  156. session.Values["redirect_uri"] = r.URL.Path
  157. } else {
  158. session.Values["redirect_uri"] = r.URL.Path + "?" + r.URL.RawQuery
  159. }
  160. session.Save(r, w)
  161. // special logic for GET /api/projects/{project_id}/invites/{token}
  162. if r.Method == "GET" && strings.Contains(r.URL.Path, "/invites/") &&
  163. !strings.HasSuffix(r.URL.Path, "/invites/") {
  164. pathSegments := strings.Split(r.URL.Path, "/")
  165. inviteToken := pathSegments[len(pathSegments)-1]
  166. invite, err := authn.config.Repo.Invite().ReadInviteByToken(inviteToken)
  167. if err != nil || invite.ProjectID == 0 || invite.Email == "" {
  168. apierrors.HandleAPIError(authn.config.Logger, authn.config.Alerter, w, r,
  169. apierrors.NewErrPassThroughToClient(fmt.Errorf("invalid invite token"), http.StatusBadRequest), true)
  170. return
  171. }
  172. if invite.IsExpired() || invite.IsAccepted() {
  173. apierrors.HandleAPIError(authn.config.Logger, authn.config.Alerter, w, r,
  174. apierrors.NewErrPassThroughToClient(fmt.Errorf("invite has expired"), http.StatusBadRequest), true)
  175. return
  176. }
  177. http.Redirect(w, r, "/register?email="+url.QueryEscape(invite.Email), http.StatusTemporaryRedirect)
  178. return
  179. }
  180. http.Redirect(w, r, "/dashboard", http.StatusFound)
  181. } else {
  182. authn.sendForbiddenError(err, w, r)
  183. }
  184. }
  185. func (authn *AuthN) verifyTokenWithNext(w http.ResponseWriter, r *http.Request, tok *token.Token) {
  186. // if the token has a stored token id we check that the token is valid in the database
  187. if tok.TokenID != "" {
  188. apiToken, err := authn.config.Repo.APIToken().ReadAPIToken(tok.ProjectID, tok.TokenID)
  189. if err != nil {
  190. authn.sendForbiddenError(fmt.Errorf("token with id %s not valid", tok.TokenID), w, r)
  191. return
  192. }
  193. // first ensure that the token hasn't been revoked, and the token has not expired
  194. if apiToken.Revoked || apiToken.IsExpired() {
  195. authn.sendForbiddenError(fmt.Errorf("token with id %s not valid", tok.TokenID), w, r)
  196. return
  197. }
  198. authn.nextWithAPIToken(w, r, apiToken)
  199. } else {
  200. // otherwise we just use nextWithUser using the `iby` field for the token
  201. authn.nextWithUserID(w, r, tok.IBy)
  202. }
  203. }
  204. // nextWithAPIToken sets the token in context
  205. func (authn *AuthN) nextWithAPIToken(w http.ResponseWriter, r *http.Request, tok *models.APIToken) {
  206. ctx := r.Context()
  207. ctx = context.WithValue(ctx, "api_token", tok)
  208. // add a service account user to the project: note that any calls depending on a DB lookup for the
  209. // user will fail
  210. ctx = context.WithValue(ctx, types.UserScope, &models.User{
  211. Email: fmt.Sprintf("%s-%d", tok.Name, tok.ProjectID),
  212. EmailVerified: true,
  213. })
  214. r = r.Clone(ctx)
  215. authn.next.ServeHTTP(w, r)
  216. }
  217. // nextWithUserID calls the next handler with the user set in the context with key
  218. // `types.UserScope`.
  219. func (authn *AuthN) nextWithUserID(w http.ResponseWriter, r *http.Request, userID uint) {
  220. // search for the user
  221. user, err := authn.config.Repo.User().ReadUser(userID)
  222. if err != nil {
  223. authn.sendForbiddenError(fmt.Errorf("user with id %d not found in database", userID), w, r)
  224. return
  225. }
  226. // add the user to the context
  227. ctx := r.Context()
  228. ctx = context.WithValue(ctx, types.UserScope, user)
  229. r = r.Clone(ctx)
  230. authn.next.ServeHTTP(w, r)
  231. }
  232. // sendForbiddenError sends a 403 Forbidden error to the end user while logging a
  233. // specific error
  234. func (authn *AuthN) sendForbiddenError(err error, w http.ResponseWriter, r *http.Request) {
  235. reqErr := apierrors.NewErrForbidden(err)
  236. apierrors.HandleAPIError(authn.config.Logger, authn.config.Alerter, w, r, reqErr, true)
  237. }
  238. var (
  239. errInvalidToken = fmt.Errorf("authorization header exists, but token is not valid")
  240. errInvalidAuthHeader = fmt.Errorf("invalid authorization header in request")
  241. )
  242. // getTokenFromRequest finds an `Authorization` header of the form `Bearer <token>`,
  243. // and returns a valid token if it exists.
  244. func (authn *AuthN) getTokenFromRequest(r *http.Request) (*token.Token, error) {
  245. reqToken := r.Header.Get("Authorization")
  246. splitToken := strings.Split(reqToken, "Bearer")
  247. if len(splitToken) != 2 {
  248. return nil, errInvalidAuthHeader
  249. }
  250. reqToken = strings.TrimSpace(splitToken[1])
  251. tok, err := token.GetTokenFromEncoded(reqToken, authn.config.TokenConf)
  252. if err != nil {
  253. return nil, fmt.Errorf("%s: %w", errInvalidToken.Error(), err)
  254. }
  255. return tok, nil
  256. }