auth
auth/authorize.go
Redirect after login. After a successful login, redirect based on a query parameter. Only allow redirections to the domain listed in the config and its subdomains. If no redirect is specified, redirect to the root of the domain listed in the config.
| 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@19 | 147 // TODO: redirect to login |
| 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@19 | 201 // TODO: redirect to login |
| 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 } |