Stub out sessions.
Stop using the Login type when getting profile by Login, removing Logins,
or recording Login use. The Login value has to be unique, anyways, and we don't
actually know the Login type when getting a profile by Login. That's sort of the
point.
Create the concept of Sessions and a sessionStore type to manage our
authentication sessions with the server. As per OWASP, we're basically just
going to use a transparent, SHA256-generated random string as an ID, and store
it client-side and server-side and just pass it back and forth.
Add the ProfileID to the Grant type, because we need to remember who granted
access. That's sort of important.
Set a defaultGrantExpiration constant to an hour, so we have that one constant
when creating new Grants.
Create a helper that pulls the session ID out of an auth cookie, checks it
against the sessionStore, and returns the Session if it's valid.
Create a helper that pulls the username and password out of a basic auth header.
Create a helper that authenticates a user's login and passphrase, checking them
against the profileStore securely.
Stub out how the cookie checking is going to work for getting grant approval.
Fix the stored Grant RedirectURI to be the passed in redirect URI, not the
RedirectURI that we ultimately redirect to. This is in accordance with the spec.
Store the profile ID from our session in the created Grant.
Stub out a GetTokenHandler that will allow users to exchange a Grant for a
Token.
Set a constant for the current passphrase scheme, which we will increment for
each revision to the passphrase scheme, for backwards compatibility.
Change the Profile iterations property to an int, not an int64, to match the
code.secondbit.org/pass library (which is matching the PBKDF2 library).
14 "code.secondbit.org/pass"
15 "code.secondbit.org/uuid"
19 authCookieName = "auth"
20 defaultGrantExpiration = 600 // default to ten minute grant expirations
21 getGrantTemplateName = "get_grant"
25 // ErrNoAuth is returned when an Authorization header is not present or is empty.
26 ErrNoAuth = errors.New("no authorization header supplied")
27 // ErrInvalidAuthFormat is returned when an Authorization header is present but not the correct format.
28 ErrInvalidAuthFormat = errors.New("authorization header is not in a valid format")
29 // ErrIncorrectAuth is returned when a user authentication attempt does not match the stored values.
30 ErrIncorrectAuth = errors.New("invalid authentication")
31 // ErrInvalidPassphraseScheme is returned when an undefined passphrase scheme is used.
32 ErrInvalidPassphraseScheme = errors.New("invalid passphrase scheme")
33 // ErrNoSession is returned when no session ID is passed with a request.
34 ErrNoSession = errors.New("no session ID found")
37 type tokenResponse struct {
38 AccessToken string `json:"access_token"`
39 TokenType string `json:"token_type,omitempty"`
40 ExpiresIn int32 `json:"expires_in,omitempty"`
41 RefreshToken string `json:"refresh_token,omitempty"`
44 func getBasicAuth(r *http.Request) (un, pass string, err error) {
45 auth := r.Header.Get("Authorization")
47 return "", "", ErrNoAuth
49 pieces := strings.SplitN(auth, " ", 2)
50 if pieces[0] != "Basic" {
51 return "", "", ErrInvalidAuthFormat
53 decoded, err := base64.StdEncoding.DecodeString(pieces[1])
55 // TODO(paddy): should probably log this...
56 return "", "", ErrInvalidAuthFormat
58 info := strings.SplitN(string(decoded), ":", 2)
59 return info[0], info[1], nil
62 func checkCookie(r *http.Request, context Context) (Session, error) {
63 cookie, err := r.Cookie(authCookieName)
65 if err == http.ErrNoCookie {
66 return Session{}, ErrNoSession
70 if cookie.Name != authCookieName || !cookie.Expires.After(time.Now()) ||
71 !cookie.Secure || !cookie.HttpOnly {
72 return Session{}, ErrInvalidSession
74 sess, err := context.GetSession(cookie.Value)
75 if err == ErrSessionNotFound {
76 return Session{}, ErrInvalidSession
77 } else if err != nil {
81 return Session{}, ErrInvalidSession
86 func authenticate(user, passphrase string, context Context) (Profile, error) {
87 profile, err := context.GetProfileByLogin(user)
89 if err == ErrProfileNotFound {
90 return Profile{}, ErrIncorrectAuth
94 switch profile.PassphraseScheme {
96 candidate := pass.Check(sha256.New, profile.Iterations, []byte(passphrase), []byte(profile.Salt))
97 if !pass.Compare(candidate, []byte(profile.Passphrase)) {
98 return Profile{}, ErrIncorrectAuth
101 // TODO(paddy): return some error
102 return Profile{}, ErrInvalidPassphraseScheme
107 // GetGrantHandler presents and processes the page for asking a user to grant access
108 // to their data. See RFC 6749, Section 4.1.
109 func GetGrantHandler(w http.ResponseWriter, r *http.Request, context Context) {
110 session, err := checkCookie(r, context)
112 if err == ErrNoSession {
113 // TODO(paddy): redirect to login screen
116 if err == ErrInvalidSession {
117 // TODO(paddy): return an access denied error
120 // TODO(paddy): return a server error
123 if r.URL.Query().Get("client_id") == "" {
124 w.WriteHeader(http.StatusBadRequest)
125 context.Render(w, getGrantTemplateName, map[string]interface{}{
126 "error": template.HTML("Client ID must be specified in the request."),
130 clientID, err := uuid.Parse(r.URL.Query().Get("client_id"))
132 w.WriteHeader(http.StatusBadRequest)
133 context.Render(w, getGrantTemplateName, map[string]interface{}{
134 "error": template.HTML("client_id is not a valid Client ID."),
138 redirectURI := r.URL.Query().Get("redirect_uri")
139 redirectURL, err := url.Parse(redirectURI)
141 w.WriteHeader(http.StatusBadRequest)
142 context.Render(w, getGrantTemplateName, map[string]interface{}{
143 "error": template.HTML("The redirect_uri specified is not valid."),
147 client, err := context.GetClient(clientID)
149 if err == ErrClientNotFound {
150 w.WriteHeader(http.StatusBadRequest)
151 context.Render(w, getGrantTemplateName, map[string]interface{}{
152 "error": template.HTML("The specified Client couldn’t be found."),
155 w.WriteHeader(http.StatusInternalServerError)
156 context.Render(w, getGrantTemplateName, map[string]interface{}{
157 "internal_error": template.HTML(err.Error()),
162 // whether a redirect URI is valid or not depends on the number of endpoints
163 // the client has registered
164 numEndpoints, err := context.CountEndpoints(clientID)
166 w.WriteHeader(http.StatusInternalServerError)
167 context.Render(w, getGrantTemplateName, map[string]interface{}{
168 "internal_error": template.HTML(err.Error()),
173 if redirectURI != "" {
174 // BUG(paddy): We really should normalize URIs before trying to compare them.
175 validURI, err = context.CheckEndpoint(clientID, redirectURI)
177 w.WriteHeader(http.StatusInternalServerError)
178 context.Render(w, getGrantTemplateName, map[string]interface{}{
179 "internal_error": template.HTML(err.Error()),
183 } else if redirectURI == "" && numEndpoints == 1 {
184 // if we don't specify the endpoint and there's only one endpoint, the
185 // request is valid, and we're redirecting to that one endpoint
187 endpoints, err := context.ListEndpoints(clientID, 1, 0)
189 w.WriteHeader(http.StatusInternalServerError)
190 context.Render(w, getGrantTemplateName, map[string]interface{}{
191 "internal_error": template.HTML(err.Error()),
195 if len(endpoints) != 1 {
198 u := endpoints[0].URI // Copy it here to avoid grabbing a pointer to the memstore
199 redirectURI = u.String()
206 w.WriteHeader(http.StatusBadRequest)
207 context.Render(w, getGrantTemplateName, map[string]interface{}{
208 "error": template.HTML("The redirect_uri specified is not valid."),
212 scope := r.URL.Query().Get("scope")
213 state := r.URL.Query().Get("state")
214 if r.URL.Query().Get("response_type") != "code" {
215 q := redirectURL.Query()
216 q.Add("error", "invalid_request")
217 q.Add("state", state)
218 redirectURL.RawQuery = q.Encode()
219 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
222 if r.Method == "POST" {
223 // BUG(paddy): We need to implement CSRF protection when obtaining a grant code.
224 if r.PostFormValue("grant") == "approved" {
225 code := uuid.NewID().String()
229 ExpiresIn: defaultGrantExpiration,
232 RedirectURI: r.URL.Query().Get("redirect_uri"),
234 ProfileID: session.ProfileID,
236 err := context.SaveGrant(grant)
238 q := redirectURL.Query()
239 q.Add("error", "server_error")
240 q.Add("state", state)
241 redirectURL.RawQuery = q.Encode()
242 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
245 q := redirectURL.Query()
247 q.Add("state", state)
248 redirectURL.RawQuery = q.Encode()
249 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
252 q := redirectURL.Query()
253 q.Add("error", "access_denied")
254 q.Add("state", state)
255 redirectURL.RawQuery = q.Encode()
256 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
259 w.WriteHeader(http.StatusOK)
260 context.Render(w, getGrantTemplateName, map[string]interface{}{
265 // GetTokenHandler allows a client to exchange an authorization grant for an
266 // access token. See RFC 6749 Section 4.1.3.
267 func GetTokenHandler(w http.ResponseWriter, r *http.Request, context Context) {
268 enc := json.NewEncoder(w)
269 grantType := r.PostFormValue("grant_type")
270 if grantType != "authorization_code" {
271 // TODO(paddy): render invalid request JSON
274 code := r.PostFormValue("code")
276 // TODO(paddy): render invalid request JSON
279 redirectURI := r.PostFormValue("redirect_uri")
280 clientIDStr, clientSecret, err := getBasicAuth(r)
282 // TODO(paddy): render access denied
285 if clientIDStr == "" && err == nil {
286 clientIDStr = r.PostFormValue("client_id")
288 // TODO(paddy): client ID can also come from Basic auth
289 clientID, err := uuid.Parse(clientIDStr)
291 // TODO(paddy): render invalid request JSON
294 client, err := context.GetClient(clientID)
296 if err == ErrClientNotFound {
297 // TODO(paddy): render invalid request JSON
299 // TODO(paddy): render internal server error JSON
303 if client.Secret != clientSecret {
304 // TODO(paddy): render invalid request JSON
307 grant, err := context.GetGrant(code)
309 if err == ErrGrantNotFound {
310 // TODO(paddy): return error
313 // TODO(paddy): return error
315 if grant.RedirectURI != redirectURI {
316 // TODO(paddy): return error
318 if !grant.ClientID.Equal(clientID) {
319 // TODO(paddy): return error
322 AccessToken: uuid.NewID().String(),
323 RefreshToken: uuid.NewID().String(),
325 ExpiresIn: defaultTokenExpiration,
326 TokenType: "", // TODO(paddy): fill in token type
328 ProfileID: grant.ProfileID,
330 err = context.SaveToken(token)
332 // TODO(paddy): return error
334 resp := tokenResponse{
335 AccessToken: token.AccessToken,
336 RefreshToken: token.RefreshToken,
337 ExpiresIn: token.ExpiresIn,
338 TokenType: token.TokenType,
340 err = enc.Encode(resp)
342 // TODO(paddy): log this or something
347 // TODO(paddy): exchange user credentials for access token
348 // TODO(paddy): exchange client credentials for access token
349 // TODO(paddy): implicit grant for access token
350 // TODO(paddy): exchange refresh token for access token