auth
auth/authorize.go
Implement CSRF prevention and pass info to confirmation. Implement CSRF prevention using the nosurf package. Note that the handler still needs to be wrapped before this will work. Pass info on the authorization being requested (namely the client and the scope) to the RenderConfirmation page so that the user can make an educated decision.
| 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@0 | 141 |
| paddy@1 | 142 if r.Method == "GET" { |
| paddy@17 | 143 ctx.RenderConfirmation(w, r, req) |
| 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@2 | 150 if err := validateSession(r, ctx); err == ErrorNotAuthenticated { |
| paddy@17 | 151 ctx.RenderLogin(w, r) |
| 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@17 | 196 ctx.RenderConfirmation(w, r, req) |
| 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@2 | 203 if err := validateSession(r, ctx); err == ErrorNotAuthenticated { |
| paddy@17 | 204 ctx.RenderLogin(w, r) |
| 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 } |