Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/public-api-server/pkg/oidc/router.go
2500 views
1
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
2
// Licensed under the GNU Affero General Public License (AGPL).
3
// See License.AGPL.txt in the project root for license information.
4
5
package oidc
6
7
import (
8
"encoding/base64"
9
"encoding/json"
10
"fmt"
11
"net/http"
12
"net/url"
13
"time"
14
15
"github.com/gitpod-io/gitpod/common-go/log"
16
17
"github.com/go-chi/chi/v5"
18
)
19
20
func Router(s *Service) *chi.Mux {
21
router := chi.NewRouter()
22
23
router.Route("/start", func(r chi.Router) {
24
r.Get("/", s.getStartHandler())
25
})
26
router.Route("/callback", func(r chi.Router) {
27
r.Use(s.OAuth2Middleware)
28
r.Get("/", s.getCallbackHandler())
29
})
30
31
return router
32
}
33
34
const (
35
stateCookieName = "state"
36
nonceCookieName = "nonce"
37
verifierCookieName = "verifier"
38
)
39
40
func (s *Service) getStartHandler() http.HandlerFunc {
41
return func(rw http.ResponseWriter, r *http.Request) {
42
useHttpErrors := false
43
if r.URL.Query().Get("useHttpErrors") != "" {
44
useHttpErrors = true
45
}
46
47
config, err := s.getClientConfigFromStartRequest(r)
48
if err != nil {
49
log.WithError(err).Warn("Failed to start SSO sign-in flow.")
50
respondeWithError(rw, r, "We were unable to find the SSO configuration you've requested. Please verify SSO is configured with your Organization owner.", http.StatusNotFound, useHttpErrors)
51
return
52
}
53
54
returnToURL := r.URL.Query().Get("returnTo")
55
if returnToURL == "" {
56
returnToURL = "/"
57
}
58
59
activate := false
60
if r.URL.Query().Get("activate") != "" {
61
activate = true
62
}
63
64
// `activate` overrides a `verify` parameter
65
verify := false
66
if !activate && r.URL.Query().Get("verify") != "" {
67
verify = true
68
}
69
70
redirectURL := getCallbackURL(r.Host)
71
72
startParams, err := s.getStartParams(config, redirectURL, StateParams{
73
ClientConfigID: config.ID,
74
ReturnToURL: returnToURL,
75
Activate: activate,
76
Verify: verify,
77
UseHttpErrors: useHttpErrors,
78
})
79
if err != nil {
80
log.WithError(err).Error("Failed to get start parameters for authentication flow.")
81
respondeWithError(rw, r, "We were unable to start the authentication flow for system reasons.", http.StatusInternalServerError, useHttpErrors)
82
return
83
}
84
85
http.SetCookie(rw, newCallbackCookie(r, nonceCookieName, startParams.Nonce))
86
http.SetCookie(rw, newCallbackCookie(r, stateCookieName, startParams.State))
87
http.SetCookie(rw, newCallbackCookie(r, verifierCookieName, startParams.CodeVerifier))
88
89
http.Redirect(rw, r, startParams.AuthCodeURL, http.StatusTemporaryRedirect)
90
}
91
}
92
93
func getCallbackURL(host string) string {
94
callbackURL := url.URL{Scheme: "https", Path: "/iam/oidc/callback", Host: host}
95
return callbackURL.String()
96
}
97
98
func newCallbackCookie(r *http.Request, name string, value string) *http.Cookie {
99
return &http.Cookie{
100
Name: name,
101
Value: value,
102
MaxAge: int(10 * time.Minute.Seconds()),
103
Secure: true,
104
SameSite: http.SameSiteLaxMode,
105
HttpOnly: true,
106
}
107
}
108
109
// The OIDC callback handler depends on the state produced in the OAuth2 middleware
110
func (s *Service) getCallbackHandler() http.HandlerFunc {
111
return func(rw http.ResponseWriter, r *http.Request) {
112
config, state, err := s.getClientConfigFromCallbackRequest(r)
113
useHttpErrors := state.UseHttpErrors
114
if err != nil {
115
log.WithError(err).Warn("Client SSO config not found")
116
reportLoginCompleted("failed_client", "sso")
117
respondeWithError(rw, r, "We were unable to find the SSO configuration you've requested. Please verify SSO is configured with your Organization owner.", http.StatusNotFound, useHttpErrors)
118
return
119
}
120
oauth2Result := GetOAuth2ResultFromContext(r.Context())
121
if oauth2Result == nil {
122
reportLoginCompleted("failed_client", "sso")
123
respondeWithError(rw, r, "OIDC precondition failure", http.StatusInternalServerError, useHttpErrors)
124
return
125
}
126
127
// nonce = number used once
128
nonceCookie, err := r.Cookie(nonceCookieName)
129
if err != nil {
130
reportLoginCompleted("failed_client", "sso")
131
respondeWithError(rw, r, "There was no nonce present on the request. Please try to sign-in in again.", http.StatusBadRequest, useHttpErrors)
132
return
133
}
134
result, err := s.authenticate(r.Context(), authenticateParams{
135
Config: config,
136
OAuth2Result: oauth2Result,
137
NonceCookieValue: nonceCookie.Value,
138
})
139
if err != nil {
140
log.WithError(err).Warn("OIDC authentication failed")
141
reportLoginCompleted("failed_client", "sso")
142
responseMsg := "We've not been able to authenticate you with the OIDC Provider."
143
if celExprErr, ok := err.(*CelExprError); ok {
144
responseMsg = fmt.Sprintf("%s [%s]", responseMsg, celExprErr.Code)
145
}
146
respondeWithError(rw, r, responseMsg, http.StatusInternalServerError, useHttpErrors)
147
return
148
}
149
150
log.WithField("id_token", result.IDToken).Trace("User verification was successful")
151
152
if state.Activate {
153
err = s.activateAndVerifyClientConfig(r.Context(), config)
154
if err != nil {
155
log.WithError(err).Warn("Failed to mark OIDC Client Config as active")
156
reportLoginCompleted("failed", "sso")
157
respondeWithError(rw, r, "We've been unable to mark the selected OIDC config as active. Please try again.", http.StatusInternalServerError, useHttpErrors)
158
return
159
}
160
}
161
162
if state.Verify {
163
// Skip the sign-in on verify-only requests.
164
err = s.markClientConfigAsVerified(r.Context(), config)
165
if err != nil {
166
log.Warn("Failed to mark config as verified: " + err.Error())
167
reportLoginCompleted("failed", "sso")
168
respondeWithError(rw, r, "Failed to mark config as verified", http.StatusInternalServerError, useHttpErrors)
169
return
170
}
171
} else {
172
cookies, _, err := s.createSession(r.Context(), result, config)
173
if err != nil {
174
log.WithError(err).Warn("Failed to create session from downstream session provider.")
175
reportLoginCompleted("failed", "sso")
176
respondeWithError(rw, r, "We were unable to create a user session.", http.StatusInternalServerError, useHttpErrors)
177
return
178
}
179
for _, cookie := range cookies {
180
http.SetCookie(rw, cookie)
181
}
182
}
183
184
reportLoginCompleted("succeeded", "sso")
185
http.Redirect(rw, r, oauth2Result.ReturnToURL, http.StatusTemporaryRedirect)
186
}
187
}
188
189
func respondeWithError(w http.ResponseWriter, r *http.Request, error string, code int, useHttpErrors bool) {
190
if useHttpErrors {
191
http.Error(w, error, code)
192
return
193
}
194
195
if !useHttpErrors {
196
jsonString, err := json.Marshal(map[string]interface{}{
197
"code": code,
198
"error": error,
199
})
200
if err != nil {
201
log.WithError(err).Warn("Failed to marchal error to be sent to frontend.")
202
jsonString = []byte("Internal error")
203
}
204
errorEncoded := base64.StdEncoding.EncodeToString(jsonString)
205
url := fmt.Sprintf("/complete-auth?message=error:%s", errorEncoded)
206
207
http.Redirect(w, r, url, http.StatusTemporaryRedirect)
208
}
209
}
210
211