aboutsummaryrefslogtreecommitdiff
path: root/app/middleware/auth.go
diff options
context:
space:
mode:
Diffstat (limited to 'app/middleware/auth.go')
-rw-r--r--app/middleware/auth.go212
1 files changed, 212 insertions, 0 deletions
diff --git a/app/middleware/auth.go b/app/middleware/auth.go
new file mode 100644
index 0000000..167d261
--- /dev/null
+++ b/app/middleware/auth.go
@@ -0,0 +1,212 @@
1package middleware
2
3import (
4 "context"
5 "fmt"
6 "net/http"
7 "strings"
8 "time"
9
10 "code.crute.us/mcrute/cloud-identity-broker/app/models"
11 "code.crute.us/mcrute/cloud-identity-broker/auth"
12 "code.crute.us/mcrute/cloud-identity-broker/auth/github"
13
14 "github.com/labstack/echo/v4"
15)
16
17// canRegisterUrls is an interface that identifies what about an HTTP router is
18// needed by this middleware. This mainly exists to work around the fact that
19// our server is actually a golib.EchoWrapper and not an echo.Echo.
20type canRegisterUrls interface {
21 GET(string, echo.HandlerFunc, ...echo.MiddlewareFunc) *echo.Route
22}
23
24const (
25 authPrincipalContextKey = "broker.AuthorizedPrincipal"
26 gitHubTokenCookie = "github-token"
27 gitHubStateCookie = "github-state"
28 oauthReturnUrl = "/github-auth"
29)
30
31// GetAuthorizedPrincipal returns the user principal object from the request
32// context and casts it correctly. Will return error if there is no principal
33// or if the principal is of the incorrect type.
34//
35// Note that use of this function implies that AuthenticationMiddleware is used
36// somewhere in the stack before the handler calling this function is
37// dispatched.
38func GetAuthorizedPrincipal(c echo.Context) (*models.User, error) {
39 rp := c.Get(authPrincipalContextKey)
40 if rp == nil {
41 return nil, fmt.Errorf("No principal set in request")
42 }
43 principal, ok := rp.(*models.User)
44 if !ok {
45 return nil, fmt.Errorf("Principal in request is not of User type")
46 }
47 return principal, nil
48}
49
50type AuthenticationMiddleware struct {
51 Store models.UserStore
52 JWTManager *auth.JWTManager
53 GitHub *github.GitHubAuthenticator
54 CookieDuration time.Duration
55}
56
57func (m *AuthenticationMiddleware) redirectToGitHubAuth(c echo.Context) error {
58 redir, state := m.GitHub.GetAuthRedirect()
59
60 c.SetCookie(&http.Cookie{
61 Name: gitHubStateCookie,
62 Value: state,
63 Path: "/",
64 Secure: true,
65 HttpOnly: true,
66 SameSite: http.SameSiteStrictMode,
67 })
68
69 return c.Redirect(http.StatusFound, redir)
70}
71
72// RegisterUrls registers the URLs required by this middleware and handler with an echo instance.
73//
74// This is here instead of in the web main because these paths are encoded in
75// the configuration for the GitHub application so changing them requires
76// addition changes to that configuration.
77func (m *AuthenticationMiddleware) RegisterUrls(e canRegisterUrls) {
78 e.GET(oauthReturnUrl, m.HandleCompleteLogin)
79}
80
81// Middleware does user authentication based on either an X-API-Key header,
82// Authorization header, or GitHub cookie depending on how the request is
83// phrased.
84//
85// If the request has either an X-API-Key or an Authorization Bearer header
86// then that must pass validation with the downstream validation logic.
87// Failures through this path are hard failures and the only way to re-try them
88// is to authenticate with a new token. The underlying assumption is that only
89// programmatic access goes through this path so redirecting to interactive
90// authentication is pointless.
91//
92// In the absence of those headers it's assumed that the user is interactive
93// and their auth cookie will be read and validated (by the exact same logic
94// that an API key is validated, they're the same format) but the failure case
95// here will redirect the user to GitHub for interactive auth.
96//
97// X-API-Key should be considered deprecated and the Authorization header with
98// a type of Bearer should be used instead. This is more in-line with Oauth 2
99// style authentication. However, for now this middleware continues to support
100// X-API-Key for to not break legacy API clients.
101func (m *AuthenticationMiddleware) Middleware(next echo.HandlerFunc) echo.HandlerFunc {
102 return func(c echo.Context) error {
103 token := c.Request().Header.Get("X-API-Key")
104 if token == "" {
105 tp := strings.Split(c.Request().Header.Get(echo.HeaderAuthorization), " ")
106 if len(tp) == 2 && tp[0] == "Bearer" {
107 token = tp[1]
108 }
109 }
110
111 // If an API key is specified this is a success or failure path. There
112 // is no option to authenticate to GitHub as would an interactive user.
113 if token != "" {
114 u, err := m.JWTManager.Validate(token)
115 if err != nil {
116 c.Logger().Debugf("Error validating JWT: %w", err)
117 return echo.ErrUnauthorized
118 }
119 c.Set(authPrincipalContextKey, u)
120 return next(c)
121 }
122
123 // Kick them to GitHub auth if they have no auth cookie
124 authCookie, err := c.Cookie(gitHubTokenCookie)
125 if err != nil {
126 return m.redirectToGitHubAuth(c)
127 }
128
129 // If they fail the check them bounce them through logout to remove
130 // their existing cookies which should then bounce them back through
131 // GitHub auth, which will eventually land them back here.
132 u, err := m.JWTManager.Validate(authCookie.Value)
133 if err != nil {
134 c.Logger().Debugf("Error validating JWT: %w", err)
135 return c.Redirect(http.StatusFound, "/logout")
136 }
137
138 c.Set(authPrincipalContextKey, u)
139 return next(c)
140 }
141}
142
143// HandleCompleteLogin handles the Oauth 2 code flow. It receives the auth code
144// and uses that to retrieve the auth token. This sets the user's auth cookie
145// to a authenticated JWT.
146//
147// This is redirected-to by the Oauth authorization server and should never be
148// hit directly by a user or script.
149func (m *AuthenticationMiddleware) HandleCompleteLogin(c echo.Context) error {
150 ctx := context.Background()
151
152 code, state := c.QueryParam("code"), c.QueryParam("state")
153 if code == "" || state == "" {
154 return echo.ErrBadRequest
155 }
156
157 ghState, err := c.Cookie(gitHubStateCookie)
158 if err != nil || ghState.Value == "" {
159 return echo.ErrBadRequest
160 }
161
162 if ghState.Value != state {
163 return echo.ErrBadRequest
164 }
165
166 token, err := m.GitHub.GetTokens(code)
167 if err != nil {
168 return echo.ErrBadRequest
169 }
170
171 user, err := m.GitHub.GetUsernameWithToken(token.AccessToken)
172 if err != nil {
173 c.Logger().Debugf("Error getting GitHub username with token: %w", err)
174 return echo.ErrUnauthorized
175 }
176
177 dbUser, err := m.Store.Get(ctx, user)
178 if err != nil {
179 c.Logger().Errorf("GitHub user %s does not have access to app", user)
180 return echo.ErrUnauthorized
181 }
182
183 jwt, sk, err := m.JWTManager.CreateForUser(dbUser)
184 if err != nil {
185 return echo.ErrInternalServerError
186 }
187
188 dbUser.AddKey(sk)
189 dbUser.GCKeys() // This is a convenient place to do it
190
191 dbUser.AddToken(&models.AuthToken{
192 Kind: "github",
193 Token: token.AccessToken,
194 RefreshToken: token.RefreshToken,
195 })
196
197 if err := m.Store.Put(ctx, dbUser); err != nil {
198 return echo.ErrInternalServerError
199 }
200
201 c.SetCookie(&http.Cookie{
202 Name: gitHubTokenCookie,
203 Value: jwt,
204 Path: "/",
205 MaxAge: int(m.CookieDuration.Seconds()),
206 Secure: true,
207 HttpOnly: true,
208 SameSite: http.SameSiteStrictMode,
209 })
210
211 return c.Redirect(http.StatusFound, "/")
212}