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