auth

Paddy 2014-11-11 Parent:ff7bf5bd0df3 Child:6fac0d6d6ca3

75:8bc51f76e717 Go to Latest

auth/oauth2.go

Typo. We need to actually compare the error to ErrInvalidSession.

History
paddy@51 1 package auth
paddy@51 2
paddy@51 3 import (
paddy@69 4 "encoding/base64"
paddy@69 5 "encoding/json"
paddy@69 6 "errors"
paddy@61 7 "html/template"
paddy@51 8 "net/http"
paddy@60 9 "net/url"
paddy@69 10 "strings"
paddy@60 11 "time"
paddy@56 12
paddy@69 13 "crypto/sha256"
paddy@69 14 "code.secondbit.org/pass"
paddy@56 15 "code.secondbit.org/uuid"
paddy@51 16 )
paddy@51 17
paddy@60 18 const (
paddy@69 19 authCookieName = "auth"
paddy@69 20 defaultGrantExpiration = 600 // default to ten minute grant expirations
paddy@60 21 getGrantTemplateName = "get_grant"
paddy@60 22 )
paddy@51 23
paddy@69 24 var (
paddy@69 25 // ErrNoAuth is returned when an Authorization header is not present or is empty.
paddy@69 26 ErrNoAuth = errors.New("no authorization header supplied")
paddy@69 27 // ErrInvalidAuthFormat is returned when an Authorization header is present but not the correct format.
paddy@69 28 ErrInvalidAuthFormat = errors.New("authorization header is not in a valid format")
paddy@69 29 // ErrIncorrectAuth is returned when a user authentication attempt does not match the stored values.
paddy@69 30 ErrIncorrectAuth = errors.New("invalid authentication")
paddy@69 31 // ErrInvalidPassphraseScheme is returned when an undefined passphrase scheme is used.
paddy@69 32 ErrInvalidPassphraseScheme = errors.New("invalid passphrase scheme")
paddy@69 33 // ErrNoSession is returned when no session ID is passed with a request.
paddy@69 34 ErrNoSession = errors.New("no session ID found")
paddy@69 35 )
paddy@69 36
paddy@69 37 type tokenResponse struct {
paddy@69 38 AccessToken string `json:"access_token"`
paddy@69 39 TokenType string `json:"token_type,omitempty"`
paddy@69 40 ExpiresIn int32 `json:"expires_in,omitempty"`
paddy@69 41 RefreshToken string `json:"refresh_token,omitempty"`
paddy@69 42 }
paddy@69 43
paddy@69 44 func getBasicAuth(r *http.Request) (un, pass string, err error) {
paddy@69 45 auth := r.Header.Get("Authorization")
paddy@69 46 if auth == "" {
paddy@69 47 return "", "", ErrNoAuth
paddy@69 48 }
paddy@69 49 pieces := strings.SplitN(auth, " ", 2)
paddy@69 50 if pieces[0] != "Basic" {
paddy@69 51 return "", "", ErrInvalidAuthFormat
paddy@69 52 }
paddy@69 53 decoded, err := base64.StdEncoding.DecodeString(pieces[1])
paddy@69 54 if err != nil {
paddy@69 55 return "", "", ErrInvalidAuthFormat
paddy@69 56 }
paddy@69 57 info := strings.SplitN(string(decoded), ":", 2)
paddy@69 58 return info[0], info[1], nil
paddy@69 59 }
paddy@69 60
paddy@69 61 func checkCookie(r *http.Request, context Context) (Session, error) {
paddy@69 62 cookie, err := r.Cookie(authCookieName)
paddy@69 63 if err != nil {
paddy@69 64 if err == http.ErrNoCookie {
paddy@69 65 return Session{}, ErrNoSession
paddy@69 66 }
paddy@69 67 return Session{}, err
paddy@69 68 }
paddy@69 69 if cookie.Name != authCookieName || !cookie.Expires.After(time.Now()) ||
paddy@69 70 !cookie.Secure || !cookie.HttpOnly {
paddy@69 71 return Session{}, ErrInvalidSession
paddy@69 72 }
paddy@69 73 sess, err := context.GetSession(cookie.Value)
paddy@69 74 if err == ErrSessionNotFound {
paddy@69 75 return Session{}, ErrInvalidSession
paddy@69 76 } else if err != nil {
paddy@69 77 return Session{}, err
paddy@69 78 }
paddy@69 79 if !sess.Active {
paddy@69 80 return Session{}, ErrInvalidSession
paddy@69 81 }
paddy@69 82 return sess, nil
paddy@69 83 }
paddy@69 84
paddy@69 85 func authenticate(user, passphrase string, context Context) (Profile, error) {
paddy@69 86 profile, err := context.GetProfileByLogin(user)
paddy@69 87 if err != nil {
paddy@69 88 if err == ErrProfileNotFound {
paddy@69 89 return Profile{}, ErrIncorrectAuth
paddy@69 90 }
paddy@69 91 return Profile{}, err
paddy@69 92 }
paddy@69 93 switch profile.PassphraseScheme {
paddy@69 94 case 1:
paddy@69 95 candidate := pass.Check(sha256.New, profile.Iterations, []byte(passphrase), []byte(profile.Salt))
paddy@69 96 if !pass.Compare(candidate, []byte(profile.Passphrase)) {
paddy@69 97 return Profile{}, ErrIncorrectAuth
paddy@69 98 }
paddy@69 99 default:
paddy@69 100 return Profile{}, ErrInvalidPassphraseScheme
paddy@69 101 }
paddy@69 102 return profile, nil
paddy@69 103 }
paddy@69 104
paddy@57 105 // GetGrantHandler presents and processes the page for asking a user to grant access
paddy@57 106 // to their data. See RFC 6749, Section 4.1.
paddy@51 107 func GetGrantHandler(w http.ResponseWriter, r *http.Request, context Context) {
paddy@69 108 session, err := checkCookie(r, context)
paddy@69 109 if err != nil {
paddy@75 110 if err == ErrNoSession || err = ErrInvalidSessior {
paddy@69 111 // TODO(paddy): redirect to login screen
paddy@69 112 //return
paddy@69 113 }
paddy@69 114 // TODO(paddy): return a server error
paddy@69 115 //return
paddy@69 116 }
paddy@56 117 if r.URL.Query().Get("client_id") == "" {
paddy@56 118 w.WriteHeader(http.StatusBadRequest)
paddy@56 119 context.Render(w, getGrantTemplateName, map[string]interface{}{
paddy@61 120 "error": template.HTML("Client ID must be specified in the request."),
paddy@56 121 })
paddy@56 122 return
paddy@56 123 }
paddy@56 124 clientID, err := uuid.Parse(r.URL.Query().Get("client_id"))
paddy@56 125 if err != nil {
paddy@56 126 w.WriteHeader(http.StatusBadRequest)
paddy@56 127 context.Render(w, getGrantTemplateName, map[string]interface{}{
paddy@61 128 "error": template.HTML("client_id is not a valid Client ID."),
paddy@56 129 })
paddy@56 130 return
paddy@56 131 }
paddy@64 132 redirectURI := r.URL.Query().Get("redirect_uri")
paddy@64 133 redirectURL, err := url.Parse(redirectURI)
paddy@64 134 if err != nil {
paddy@64 135 w.WriteHeader(http.StatusBadRequest)
paddy@64 136 context.Render(w, getGrantTemplateName, map[string]interface{}{
paddy@64 137 "error": template.HTML("The redirect_uri specified is not valid."),
paddy@64 138 })
paddy@64 139 return
paddy@64 140 }
paddy@56 141 client, err := context.GetClient(clientID)
paddy@56 142 if err != nil {
paddy@59 143 if err == ErrClientNotFound {
paddy@59 144 w.WriteHeader(http.StatusBadRequest)
paddy@59 145 context.Render(w, getGrantTemplateName, map[string]interface{}{
paddy@61 146 "error": template.HTML("The specified Client couldn’t be found."),
paddy@59 147 })
paddy@59 148 } else {
paddy@59 149 w.WriteHeader(http.StatusInternalServerError)
paddy@59 150 context.Render(w, getGrantTemplateName, map[string]interface{}{
paddy@61 151 "internal_error": template.HTML(err.Error()),
paddy@59 152 })
paddy@59 153 }
paddy@56 154 return
paddy@56 155 }
paddy@56 156 // whether a redirect URI is valid or not depends on the number of endpoints
paddy@56 157 // the client has registered
paddy@56 158 numEndpoints, err := context.CountEndpoints(clientID)
paddy@56 159 if err != nil {
paddy@56 160 w.WriteHeader(http.StatusInternalServerError)
paddy@56 161 context.Render(w, getGrantTemplateName, map[string]interface{}{
paddy@61 162 "internal_error": template.HTML(err.Error()),
paddy@56 163 })
paddy@56 164 return
paddy@56 165 }
paddy@56 166 var validURI bool
paddy@58 167 if redirectURI != "" {
paddy@58 168 // BUG(paddy): We really should normalize URIs before trying to compare them.
paddy@58 169 validURI, err = context.CheckEndpoint(clientID, redirectURI)
paddy@56 170 if err != nil {
paddy@56 171 w.WriteHeader(http.StatusInternalServerError)
paddy@56 172 context.Render(w, getGrantTemplateName, map[string]interface{}{
paddy@61 173 "internal_error": template.HTML(err.Error()),
paddy@56 174 })
paddy@56 175 return
paddy@56 176 }
paddy@56 177 } else if redirectURI == "" && numEndpoints == 1 {
paddy@56 178 // if we don't specify the endpoint and there's only one endpoint, the
paddy@56 179 // request is valid, and we're redirecting to that one endpoint
paddy@56 180 validURI = true
paddy@56 181 endpoints, err := context.ListEndpoints(clientID, 1, 0)
paddy@56 182 if err != nil {
paddy@56 183 w.WriteHeader(http.StatusInternalServerError)
paddy@56 184 context.Render(w, getGrantTemplateName, map[string]interface{}{
paddy@61 185 "internal_error": template.HTML(err.Error()),
paddy@56 186 })
paddy@56 187 return
paddy@56 188 }
paddy@56 189 if len(endpoints) != 1 {
paddy@56 190 validURI = false
paddy@56 191 } else {
paddy@66 192 u := endpoints[0].URI // Copy it here to avoid grabbing a pointer to the memstore
paddy@66 193 redirectURI = u.String()
paddy@66 194 redirectURL = &u
paddy@56 195 }
paddy@56 196 } else {
paddy@56 197 validURI = false
paddy@56 198 }
paddy@56 199 if !validURI {
paddy@56 200 w.WriteHeader(http.StatusBadRequest)
paddy@56 201 context.Render(w, getGrantTemplateName, map[string]interface{}{
paddy@61 202 "error": template.HTML("The redirect_uri specified is not valid."),
paddy@56 203 })
paddy@56 204 return
paddy@56 205 }
paddy@60 206 scope := r.URL.Query().Get("scope")
paddy@60 207 state := r.URL.Query().Get("state")
paddy@56 208 if r.URL.Query().Get("response_type") != "code" {
paddy@65 209 q := redirectURL.Query()
paddy@65 210 q.Add("error", "invalid_request")
paddy@65 211 q.Add("state", state)
paddy@65 212 redirectURL.RawQuery = q.Encode()
paddy@60 213 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
paddy@60 214 return
paddy@56 215 }
paddy@56 216 if r.Method == "POST" {
paddy@63 217 // BUG(paddy): We need to implement CSRF protection when obtaining a grant code.
paddy@56 218 if r.PostFormValue("grant") == "approved" {
paddy@60 219 code := uuid.NewID().String()
paddy@60 220 grant := Grant{
paddy@60 221 Code: code,
paddy@60 222 Created: time.Now(),
paddy@60 223 ExpiresIn: defaultGrantExpiration,
paddy@60 224 ClientID: clientID,
paddy@60 225 Scope: scope,
paddy@69 226 RedirectURI: r.URL.Query().Get("redirect_uri"),
paddy@60 227 State: state,
paddy@69 228 ProfileID: session.ProfileID,
paddy@60 229 }
paddy@60 230 err := context.SaveGrant(grant)
paddy@60 231 if err != nil {
paddy@66 232 q := redirectURL.Query()
paddy@66 233 q.Add("error", "server_error")
paddy@66 234 q.Add("state", state)
paddy@66 235 redirectURL.RawQuery = q.Encode()
paddy@60 236 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
paddy@60 237 return
paddy@60 238 }
paddy@66 239 q := redirectURL.Query()
paddy@66 240 q.Add("code", code)
paddy@66 241 q.Add("state", state)
paddy@66 242 redirectURL.RawQuery = q.Encode()
paddy@60 243 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
paddy@60 244 return
paddy@56 245 }
paddy@66 246 q := redirectURL.Query()
paddy@66 247 q.Add("error", "access_denied")
paddy@66 248 q.Add("state", state)
paddy@66 249 redirectURL.RawQuery = q.Encode()
paddy@60 250 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
paddy@60 251 return
paddy@56 252 }
paddy@51 253 w.WriteHeader(http.StatusOK)
paddy@56 254 context.Render(w, getGrantTemplateName, map[string]interface{}{
paddy@56 255 "client": client,
paddy@56 256 })
paddy@51 257 }
paddy@68 258
paddy@69 259 // GetTokenHandler allows a client to exchange an authorization grant for an
paddy@69 260 // access token. See RFC 6749 Section 4.1.3.
paddy@69 261 func GetTokenHandler(w http.ResponseWriter, r *http.Request, context Context) {
paddy@69 262 enc := json.NewEncoder(w)
paddy@69 263 grantType := r.PostFormValue("grant_type")
paddy@69 264 if grantType != "authorization_code" {
paddy@69 265 // TODO(paddy): render invalid request JSON
paddy@69 266 return
paddy@69 267 }
paddy@69 268 code := r.PostFormValue("code")
paddy@69 269 if code == "" {
paddy@69 270 // TODO(paddy): render invalid request JSON
paddy@69 271 return
paddy@69 272 }
paddy@69 273 redirectURI := r.PostFormValue("redirect_uri")
paddy@69 274 clientIDStr, clientSecret, err := getBasicAuth(r)
paddy@69 275 if err != nil {
paddy@69 276 // TODO(paddy): render access denied
paddy@69 277 return
paddy@69 278 }
paddy@69 279 if clientIDStr == "" && err == nil {
paddy@69 280 clientIDStr = r.PostFormValue("client_id")
paddy@69 281 }
paddy@69 282 clientID, err := uuid.Parse(clientIDStr)
paddy@69 283 if err != nil {
paddy@69 284 // TODO(paddy): render invalid request JSON
paddy@69 285 return
paddy@69 286 }
paddy@69 287 client, err := context.GetClient(clientID)
paddy@69 288 if err != nil {
paddy@69 289 if err == ErrClientNotFound {
paddy@69 290 // TODO(paddy): render invalid request JSON
paddy@69 291 } else {
paddy@69 292 // TODO(paddy): render internal server error JSON
paddy@69 293 }
paddy@69 294 return
paddy@69 295 }
paddy@69 296 if client.Secret != clientSecret {
paddy@69 297 // TODO(paddy): render invalid request JSON
paddy@69 298 return
paddy@69 299 }
paddy@69 300 grant, err := context.GetGrant(code)
paddy@69 301 if err != nil {
paddy@69 302 if err == ErrGrantNotFound {
paddy@69 303 // TODO(paddy): return error
paddy@69 304 return
paddy@69 305 }
paddy@69 306 // TODO(paddy): return error
paddy@69 307 }
paddy@69 308 if grant.RedirectURI != redirectURI {
paddy@69 309 // TODO(paddy): return error
paddy@69 310 }
paddy@69 311 if !grant.ClientID.Equal(clientID) {
paddy@69 312 // TODO(paddy): return error
paddy@69 313 }
paddy@69 314 token := Token{
paddy@69 315 AccessToken: uuid.NewID().String(),
paddy@69 316 RefreshToken: uuid.NewID().String(),
paddy@69 317 Created: time.Now(),
paddy@69 318 ExpiresIn: defaultTokenExpiration,
paddy@69 319 TokenType: "", // TODO(paddy): fill in token type
paddy@69 320 Scope: grant.Scope,
paddy@69 321 ProfileID: grant.ProfileID,
paddy@69 322 }
paddy@69 323 err = context.SaveToken(token)
paddy@69 324 if err != nil {
paddy@69 325 // TODO(paddy): return error
paddy@69 326 }
paddy@69 327 resp := tokenResponse{
paddy@69 328 AccessToken: token.AccessToken,
paddy@69 329 RefreshToken: token.RefreshToken,
paddy@69 330 ExpiresIn: token.ExpiresIn,
paddy@69 331 TokenType: token.TokenType,
paddy@69 332 }
paddy@69 333 err = enc.Encode(resp)
paddy@69 334 if err != nil {
paddy@69 335 // TODO(paddy): log this or something
paddy@69 336 return
paddy@69 337 }
paddy@69 338 }
paddy@69 339
paddy@68 340 // TODO(paddy): exchange user credentials for access token
paddy@68 341 // TODO(paddy): exchange client credentials for access token
paddy@68 342 // TODO(paddy): implicit grant for access token
paddy@68 343 // TODO(paddy): exchange refresh token for access token