auth
auth/authorize.go
Continue our descent to horribleness. Remove all the nonsense about "extensibility" and "clean separation of concerns", instead hardcoding connections to decisions. Remove all those "test" things that stopped passing.
| paddy@0 | 1 package oauth2 |
| 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@0 | 141 |
| paddy@1 | 142 if r.Method == "GET" { |
| paddy@1 | 143 ctx.RenderConfirmation(w) |
| paddy@0 | 144 return |
| paddy@1 | 145 } else if r.Method != "POST" { |
| paddy@1 | 146 ctx.RenderError(w, InvalidMethodError) |
| paddy@0 | 147 return |
| paddy@0 | 148 } |
| paddy@0 | 149 |
| paddy@1 | 150 if err := validateSession(r); err == ErrorNotAuthenticated { |
| paddy@1 | 151 ctx.RenderLogin(w) |
| paddy@1 | 152 return |
| paddy@1 | 153 } else if err != nil { |
| paddy@1 | 154 ctx.RenderError(w, err) |
| paddy@0 | 155 return |
| paddy@0 | 156 } |
| paddy@0 | 157 |
| paddy@1 | 158 if r.FormValue("approved") != "true" { |
| paddy@1 | 159 redir, err := req.GetErrorRedirect(ErrorAccessDenied, "Request was not authorized.", ctx.Config.DocumentationDomain) |
| paddy@1 | 160 if err != nil { |
| paddy@1 | 161 ctx.RenderError(w, URIFormatError(req.RedirectURI)) |
| paddy@1 | 162 return |
| paddy@1 | 163 } |
| paddy@1 | 164 http.Redirect(w, r, redir, http.StatusFound) |
| paddy@0 | 165 return |
| paddy@0 | 166 } |
| paddy@0 | 167 |
| paddy@1 | 168 data := AuthorizeData{AuthRequest: req} |
| paddy@1 | 169 |
| paddy@1 | 170 data.ExpiresIn = ctx.Config.AuthorizationExpiration |
| paddy@1 | 171 data.Code = newToken() |
| paddy@1 | 172 data.CreatedAt = time.Now() |
| paddy@1 | 173 |
| paddy@1 | 174 err := ctx.Tokens.SaveAuthorization(data) |
| paddy@1 | 175 if err != nil { |
| paddy@1 | 176 redir, err := req.GetErrorRedirect(ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain) |
| paddy@1 | 177 if err != nil { |
| paddy@1 | 178 ctx.RenderError(w, URIFormatError(req.RedirectURI)) |
| paddy@1 | 179 return |
| paddy@1 | 180 } |
| paddy@1 | 181 http.Redirect(w, r, redir, http.StatusFound) |
| paddy@1 | 182 return |
| paddy@0 | 183 } |
| paddy@0 | 184 |
| paddy@1 | 185 redir, err := data.GetRedirect() |
| paddy@1 | 186 if err != nil { |
| paddy@1 | 187 ctx.RenderError(w, URIFormatError(req.RedirectURI)) |
| paddy@1 | 188 return |
| paddy@1 | 189 } |
| paddy@1 | 190 http.Redirect(w, r, redir, http.StatusFound) |
| paddy@0 | 191 } |
| paddy@0 | 192 |
| paddy@1 | 193 func (req AuthRequest) handleTokenRequest(w http.ResponseWriter, r *http.Request, ctx Context) { |
| paddy@0 | 194 |
| paddy@1 | 195 if r.Method == "GET" { |
| paddy@1 | 196 ctx.RenderConfirmation(w) |
| paddy@1 | 197 return |
| paddy@1 | 198 } else if r.Method != "POST" { |
| paddy@1 | 199 ctx.RenderError(w, InvalidMethodError) |
| paddy@1 | 200 return |
| paddy@1 | 201 } |
| paddy@0 | 202 |
| paddy@1 | 203 if err := validateSession(r); err == ErrorNotAuthenticated { |
| paddy@1 | 204 ctx.RenderLogin(w) |
| paddy@1 | 205 return |
| paddy@1 | 206 } else if err != nil { |
| paddy@1 | 207 ctx.RenderError(w, err) |
| paddy@1 | 208 return |
| paddy@1 | 209 } |
| paddy@1 | 210 |
| paddy@1 | 211 if r.FormValue("approved") != "true" { |
| paddy@1 | 212 redir, err := req.GetErrorRedirect(ErrorAccessDenied, "Request was not authorized.", ctx.Config.DocumentationDomain) |
| paddy@0 | 213 if err != nil { |
| paddy@1 | 214 ctx.RenderError(w, URIFormatError(req.RedirectURI)) |
| paddy@0 | 215 return |
| paddy@0 | 216 } |
| paddy@1 | 217 http.Redirect(w, r, redir, http.StatusFound) |
| paddy@1 | 218 return |
| paddy@1 | 219 } |
| paddy@0 | 220 |
| paddy@1 | 221 data := AccessData{AuthRequest: req} |
| paddy@1 | 222 |
| paddy@1 | 223 err := fillTokens(&data, false, ctx) |
| paddy@1 | 224 if err != nil { |
| paddy@1 | 225 ctx.RenderError(w, InternalServerError) |
| paddy@1 | 226 return |
| paddy@0 | 227 } |
| paddy@1 | 228 |
| paddy@1 | 229 redir, err := data.GetRedirect(true) |
| paddy@1 | 230 if err != nil { |
| paddy@1 | 231 ctx.RenderError(w, URIFormatError(req.RedirectURI)) |
| paddy@1 | 232 return |
| paddy@1 | 233 } |
| paddy@1 | 234 http.Redirect(w, r, redir, http.StatusFound) |
| paddy@0 | 235 } |
| paddy@0 | 236 |
| paddy@1 | 237 func (data AuthorizeData) GetRedirect() (string, error) { |
| paddy@1 | 238 u, err := url.Parse(data.RedirectURI) |
| paddy@1 | 239 if err != nil { |
| paddy@1 | 240 return "", err |
| paddy@1 | 241 } |
| paddy@1 | 242 |
| paddy@1 | 243 // add parameters |
| paddy@1 | 244 q := u.Query() |
| paddy@1 | 245 q.Set("code", data.Code) |
| paddy@1 | 246 q.Set("state", data.State) |
| paddy@1 | 247 u.RawQuery = q.Encode() |
| paddy@1 | 248 |
| paddy@1 | 249 return u.String(), nil |
| paddy@0 | 250 } |
| paddy@0 | 251 |
| paddy@1 | 252 func (req AuthRequest) GetErrorRedirect(code, description, uriBase string) (string, error) { |
| paddy@1 | 253 u, err := url.Parse(req.RedirectURI) |
| paddy@1 | 254 if err != nil { |
| paddy@1 | 255 return "", err |
| paddy@1 | 256 } |
| paddy@1 | 257 |
| paddy@1 | 258 // add parameters |
| paddy@1 | 259 q := u.Query() |
| paddy@1 | 260 q.Set("error", code) |
| paddy@1 | 261 q.Set("error_description", description) |
| paddy@1 | 262 q.Set("error_uri", strings.Join([]string{ |
| paddy@1 | 263 strings.TrimRight(uriBase, "/"), |
| paddy@1 | 264 strings.TrimLeft(code, "/"), |
| paddy@1 | 265 }, "/")) |
| paddy@1 | 266 q.Set("state", req.State) |
| paddy@1 | 267 u.RawQuery = q.Encode() |
| paddy@1 | 268 |
| paddy@1 | 269 return u.String(), nil |
| paddy@0 | 270 } |