auth
2014-09-01
Parent:1aa3a85ff853
auth/authorize.go.old
Implement GrantStore for Memstore. Fix bug in GrantStore that asks for a UUID when it really wants a string. Implement GrantStore interface in Memstore, including unit tests for successful (i.e., works-as-intended) scenarios.
| paddy@23 | 1 package auth |
| paddy@23 | 2 |
| paddy@23 | 3 import ( |
| paddy@23 | 4 "net/http" |
| paddy@23 | 5 "net/url" |
| paddy@23 | 6 "time" |
| paddy@23 | 7 |
| paddy@23 | 8 "strings" |
| paddy@23 | 9 "secondbit.org/uuid" |
| paddy@23 | 10 ) |
| paddy@23 | 11 |
| paddy@23 | 12 // AuthorizeRequestType is the type for OAuth param `response_type` |
| paddy@23 | 13 type AuthorizeRequestType string |
| paddy@23 | 14 |
| paddy@23 | 15 const ( |
| paddy@23 | 16 CodeAuthRT AuthorizeRequestType = "code" |
| paddy@23 | 17 TokenAuthRT = "token" |
| paddy@23 | 18 ) |
| paddy@23 | 19 |
| paddy@23 | 20 type AuthRequest struct { |
| paddy@23 | 21 Client Client |
| paddy@23 | 22 Scope string |
| paddy@23 | 23 RedirectURI string |
| paddy@23 | 24 State string |
| paddy@23 | 25 } |
| paddy@23 | 26 |
| paddy@23 | 27 // Authorization data |
| paddy@23 | 28 type AuthorizeData struct { |
| paddy@23 | 29 // Authorization code |
| paddy@23 | 30 Code string |
| paddy@23 | 31 |
| paddy@23 | 32 // Token expiration in seconds |
| paddy@23 | 33 ExpiresIn int32 |
| paddy@23 | 34 |
| paddy@23 | 35 // Date created |
| paddy@23 | 36 CreatedAt time.Time |
| paddy@23 | 37 |
| paddy@23 | 38 ProfileID uuid.ID |
| paddy@23 | 39 |
| paddy@23 | 40 AuthRequest |
| paddy@23 | 41 } |
| paddy@23 | 42 |
| paddy@23 | 43 // IsExpired is true if authorization expired |
| paddy@23 | 44 func (d *AuthorizeData) IsExpired() bool { |
| paddy@23 | 45 return d.CreatedAt.Add(time.Duration(d.ExpiresIn) * time.Second).Before(time.Now()) |
| paddy@23 | 46 } |
| paddy@23 | 47 |
| paddy@23 | 48 // ExpireAt returns the expiration date |
| paddy@23 | 49 func (d *AuthorizeData) ExpireAt() time.Time { |
| paddy@23 | 50 return d.CreatedAt.Add(time.Duration(d.ExpiresIn) * time.Second) |
| paddy@23 | 51 } |
| paddy@23 | 52 |
| paddy@23 | 53 // HandleAuthorizeRequest is the main http.HandlerFunc for handling |
| paddy@23 | 54 // authorization requests |
| paddy@23 | 55 func HandleAuthorizeRequest(w http.ResponseWriter, r *http.Request, ctx Context) { |
| paddy@23 | 56 r.ParseForm() |
| paddy@23 | 57 // create the authorization request |
| paddy@23 | 58 redirectURI := r.Form.Get("redirect_uri") |
| paddy@23 | 59 var err error |
| paddy@23 | 60 if redirectURI != "" { |
| paddy@23 | 61 redirectURI, err = url.QueryUnescape(redirectURI) |
| paddy@23 | 62 if err != nil { |
| paddy@23 | 63 ctx.RenderError(w, URIFormatError(redirectURI)) |
| paddy@23 | 64 return |
| paddy@23 | 65 } |
| paddy@23 | 66 } |
| paddy@23 | 67 |
| paddy@23 | 68 state := r.Form.Get("state") |
| paddy@23 | 69 scope := r.Form.Get("scope") |
| paddy@23 | 70 |
| paddy@23 | 71 // must have a valid client |
| paddy@23 | 72 id, err := uuid.Parse(r.Form.Get("client_id")) |
| paddy@23 | 73 if err != nil { |
| paddy@23 | 74 ctx.RenderError(w, InvalidClientIDError(r.Form.Get("client_id"))) |
| paddy@23 | 75 return |
| paddy@23 | 76 } |
| paddy@23 | 77 client, err := GetClient(id, ctx) |
| paddy@23 | 78 if err != nil { |
| paddy@23 | 79 if err == ClientNotFoundError { |
| paddy@23 | 80 ctx.RenderError(w, ClientNotFoundError) |
| paddy@23 | 81 return |
| paddy@23 | 82 } |
| paddy@23 | 83 if redirectURI == "" { |
| paddy@23 | 84 ctx.RenderError(w, URIMissingError) |
| paddy@23 | 85 return |
| paddy@23 | 86 } |
| paddy@23 | 87 req := AuthRequest{ |
| paddy@23 | 88 RedirectURI: redirectURI, |
| paddy@23 | 89 Scope: scope, |
| paddy@23 | 90 State: state, |
| paddy@23 | 91 } |
| paddy@23 | 92 redir, err := req.GetErrorRedirect(ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain) |
| paddy@23 | 93 if err != nil { |
| paddy@23 | 94 ctx.RenderError(w, URIFormatError(redirectURI)) |
| paddy@23 | 95 return |
| paddy@23 | 96 } |
| paddy@23 | 97 http.Redirect(w, r, redir, http.StatusFound) |
| paddy@23 | 98 return |
| paddy@23 | 99 } |
| paddy@23 | 100 if client.RedirectURI == "" { |
| paddy@23 | 101 ctx.RenderError(w, URIMissingError) |
| paddy@23 | 102 return |
| paddy@23 | 103 } |
| paddy@23 | 104 |
| paddy@23 | 105 // check redirect uri |
| paddy@23 | 106 if redirectURI == "" { |
| paddy@23 | 107 redirectURI = client.RedirectURI |
| paddy@23 | 108 } |
| paddy@23 | 109 if err = validateURI(client.RedirectURI, redirectURI); err != nil { |
| paddy@23 | 110 ctx.RenderError(w, NewURIMismatchError(client.RedirectURI, redirectURI)) |
| paddy@23 | 111 return |
| paddy@23 | 112 } |
| paddy@23 | 113 |
| paddy@23 | 114 req := AuthRequest{ |
| paddy@23 | 115 Client: client, |
| paddy@23 | 116 RedirectURI: redirectURI, |
| paddy@23 | 117 Scope: scope, |
| paddy@23 | 118 State: state, |
| paddy@23 | 119 } |
| paddy@23 | 120 |
| paddy@23 | 121 requestType := AuthorizeRequestType(r.Form.Get("response_type")) |
| paddy@23 | 122 if ctx.Config.AllowedAuthorizeTypes.Exists(requestType) { |
| paddy@23 | 123 switch requestType { |
| paddy@23 | 124 case CodeAuthRT: |
| paddy@23 | 125 req.handleCodeRequest(w, r, ctx) |
| paddy@23 | 126 return |
| paddy@23 | 127 case TokenAuthRT: |
| paddy@23 | 128 req.handleTokenRequest(w, r, ctx) |
| paddy@23 | 129 return |
| paddy@23 | 130 } |
| paddy@23 | 131 } |
| paddy@23 | 132 redir, err := req.GetErrorRedirect(ErrorInvalidRequest, "Invalid response type.", ctx.Config.DocumentationDomain) |
| paddy@23 | 133 if err != nil { |
| paddy@23 | 134 ctx.RenderError(w, URIFormatError(req.RedirectURI)) |
| paddy@23 | 135 return |
| paddy@23 | 136 } |
| paddy@23 | 137 http.Redirect(w, r, redir, http.StatusFound) |
| paddy@23 | 138 } |
| paddy@23 | 139 |
| paddy@23 | 140 func (req AuthRequest) handleCodeRequest(w http.ResponseWriter, r *http.Request, ctx Context) { |
| paddy@23 | 141 if r.Method != "GET" && r.Method != "POST" { |
| paddy@23 | 142 ctx.RenderError(w, InvalidMethodError) |
| paddy@23 | 143 return |
| paddy@23 | 144 } |
| paddy@23 | 145 |
| paddy@23 | 146 if err := validateSession(r, ctx); err == ErrorNotAuthenticated { |
| paddy@23 | 147 http.Redirect(w, r, "/auth/login?redirect_to="+url.QueryEscape(r.URL.String()), http.StatusFound) |
| paddy@23 | 148 return |
| paddy@23 | 149 } else if err != nil { |
| paddy@23 | 150 ctx.RenderError(w, err) |
| paddy@23 | 151 return |
| paddy@23 | 152 } |
| paddy@23 | 153 |
| paddy@23 | 154 if r.Method == "GET" { |
| paddy@23 | 155 ctx.RenderConfirmation(w, r, req) |
| paddy@23 | 156 return |
| paddy@23 | 157 } |
| paddy@23 | 158 |
| paddy@23 | 159 if r.FormValue("approved") != "true" { |
| paddy@23 | 160 redir, err := req.GetErrorRedirect(ErrorAccessDenied, "Request was not authorized.", ctx.Config.DocumentationDomain) |
| paddy@23 | 161 if err != nil { |
| paddy@23 | 162 ctx.RenderError(w, URIFormatError(req.RedirectURI)) |
| paddy@23 | 163 return |
| paddy@23 | 164 } |
| paddy@23 | 165 http.Redirect(w, r, redir, http.StatusFound) |
| paddy@23 | 166 return |
| paddy@23 | 167 } |
| paddy@23 | 168 |
| paddy@23 | 169 data := AuthorizeData{AuthRequest: req} |
| paddy@23 | 170 |
| paddy@23 | 171 data.ExpiresIn = ctx.Config.AuthorizationExpiration |
| paddy@23 | 172 data.Code = newToken() |
| paddy@23 | 173 data.CreatedAt = time.Now() |
| paddy@23 | 174 |
| paddy@23 | 175 err := ctx.Tokens.SaveAuthorization(data) |
| paddy@23 | 176 if err != nil { |
| paddy@23 | 177 redir, err := req.GetErrorRedirect(ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain) |
| paddy@23 | 178 if err != nil { |
| paddy@23 | 179 ctx.RenderError(w, URIFormatError(req.RedirectURI)) |
| paddy@23 | 180 return |
| paddy@23 | 181 } |
| paddy@23 | 182 http.Redirect(w, r, redir, http.StatusFound) |
| paddy@23 | 183 return |
| paddy@23 | 184 } |
| paddy@23 | 185 |
| paddy@23 | 186 redir, err := data.GetRedirect() |
| paddy@23 | 187 if err != nil { |
| paddy@23 | 188 ctx.RenderError(w, URIFormatError(req.RedirectURI)) |
| paddy@23 | 189 return |
| paddy@23 | 190 } |
| paddy@23 | 191 http.Redirect(w, r, redir, http.StatusFound) |
| paddy@23 | 192 } |
| paddy@23 | 193 |
| paddy@23 | 194 func (req AuthRequest) handleTokenRequest(w http.ResponseWriter, r *http.Request, ctx Context) { |
| paddy@23 | 195 if r.Method != "GET" && r.Method != "POST" { |
| paddy@23 | 196 ctx.RenderError(w, InvalidMethodError) |
| paddy@23 | 197 return |
| paddy@23 | 198 } |
| paddy@23 | 199 |
| paddy@23 | 200 if err := validateSession(r, ctx); err == ErrorNotAuthenticated { |
| paddy@23 | 201 http.Redirect(w, r, "/auth/login?redirect_to="+url.QueryEscape(r.URL.String()), http.StatusFound) |
| paddy@23 | 202 return |
| paddy@23 | 203 } else if err != nil { |
| paddy@23 | 204 ctx.RenderError(w, err) |
| paddy@23 | 205 return |
| paddy@23 | 206 } |
| paddy@23 | 207 |
| paddy@23 | 208 if r.Method == "GET" { |
| paddy@23 | 209 ctx.RenderConfirmation(w, r, req) |
| paddy@23 | 210 return |
| paddy@23 | 211 } |
| paddy@23 | 212 |
| paddy@23 | 213 if r.FormValue("approved") != "true" { |
| paddy@23 | 214 redir, err := req.GetErrorRedirect(ErrorAccessDenied, "Request was not authorized.", ctx.Config.DocumentationDomain) |
| paddy@23 | 215 if err != nil { |
| paddy@23 | 216 ctx.RenderError(w, URIFormatError(req.RedirectURI)) |
| paddy@23 | 217 return |
| paddy@23 | 218 } |
| paddy@23 | 219 http.Redirect(w, r, redir, http.StatusFound) |
| paddy@23 | 220 return |
| paddy@23 | 221 } |
| paddy@23 | 222 |
| paddy@23 | 223 data := AccessData{AuthRequest: req} |
| paddy@23 | 224 |
| paddy@23 | 225 err := fillTokens(&data, false, ctx) |
| paddy@23 | 226 if err != nil { |
| paddy@23 | 227 ctx.RenderError(w, InternalServerError) |
| paddy@23 | 228 return |
| paddy@23 | 229 } |
| paddy@23 | 230 |
| paddy@23 | 231 redir, err := data.GetRedirect(true) |
| paddy@23 | 232 if err != nil { |
| paddy@23 | 233 ctx.RenderError(w, URIFormatError(req.RedirectURI)) |
| paddy@23 | 234 return |
| paddy@23 | 235 } |
| paddy@23 | 236 http.Redirect(w, r, redir, http.StatusFound) |
| paddy@23 | 237 } |
| paddy@23 | 238 |
| paddy@23 | 239 func (data AuthorizeData) GetRedirect() (string, error) { |
| paddy@23 | 240 u, err := url.Parse(data.RedirectURI) |
| paddy@23 | 241 if err != nil { |
| paddy@23 | 242 return "", err |
| paddy@23 | 243 } |
| paddy@23 | 244 |
| paddy@23 | 245 // add parameters |
| paddy@23 | 246 q := u.Query() |
| paddy@23 | 247 q.Set("code", data.Code) |
| paddy@23 | 248 q.Set("state", data.State) |
| paddy@23 | 249 u.RawQuery = q.Encode() |
| paddy@23 | 250 |
| paddy@23 | 251 return u.String(), nil |
| paddy@23 | 252 } |
| paddy@23 | 253 |
| paddy@23 | 254 func (req AuthRequest) GetErrorRedirect(code, description, uriBase string) (string, error) { |
| paddy@23 | 255 u, err := url.Parse(req.RedirectURI) |
| paddy@23 | 256 if err != nil { |
| paddy@23 | 257 return "", err |
| paddy@23 | 258 } |
| paddy@23 | 259 |
| paddy@23 | 260 // add parameters |
| paddy@23 | 261 q := u.Query() |
| paddy@23 | 262 q.Set("error", code) |
| paddy@23 | 263 q.Set("error_description", description) |
| paddy@23 | 264 q.Set("error_uri", strings.Join([]string{ |
| paddy@23 | 265 strings.TrimRight(uriBase, "/"), |
| paddy@23 | 266 strings.TrimLeft(code, "/"), |
| paddy@23 | 267 }, "/")) |
| paddy@23 | 268 q.Set("state", req.State) |
| paddy@23 | 269 u.RawQuery = q.Encode() |
| paddy@23 | 270 |
| paddy@23 | 271 return u.String(), nil |
| paddy@23 | 272 } |