auth
auth/access.go
Add a bunch of TODOs. Let's be realistic about what still needs to be done and tag it appropriately.
| paddy@6 | 1 package auth |
| paddy@0 | 2 |
| paddy@0 | 3 import ( |
| paddy@0 | 4 "net/http" |
| paddy@1 | 5 "net/url" |
| paddy@0 | 6 "time" |
| paddy@0 | 7 |
| paddy@1 | 8 "strconv" |
| paddy@0 | 9 "secondbit.org/uuid" |
| paddy@0 | 10 ) |
| paddy@0 | 11 |
| paddy@0 | 12 // GrantType is the type for OAuth param `grant_type` |
| paddy@0 | 13 type GrantType string |
| paddy@0 | 14 |
| paddy@0 | 15 const ( |
| paddy@0 | 16 AuthorizationCodeGrant GrantType = "authorization_code" |
| paddy@0 | 17 RefreshTokenGrant = "refresh_token" |
| paddy@0 | 18 PasswordGrant = "password" |
| paddy@0 | 19 ClientCredentialsGrant = "client_credentials" |
| paddy@0 | 20 ) |
| paddy@0 | 21 |
| paddy@0 | 22 // AccessData represents an access grant (tokens, expiration, client, etc) |
| paddy@0 | 23 type AccessData struct { |
| paddy@1 | 24 PreviousAuthorizeData *AuthorizeData |
| paddy@1 | 25 PreviousAccessData *AccessData // previous access data, when refreshing |
| paddy@1 | 26 AccessToken string |
| paddy@1 | 27 RefreshToken string |
| paddy@1 | 28 ExpiresIn int32 |
| paddy@1 | 29 CreatedAt time.Time |
| paddy@1 | 30 TokenType string |
| paddy@1 | 31 ProfileID uuid.ID |
| paddy@1 | 32 AuthRequest |
| paddy@0 | 33 } |
| paddy@0 | 34 |
| paddy@0 | 35 // IsExpired returns true if access expired |
| paddy@0 | 36 func (d *AccessData) IsExpired() bool { |
| paddy@0 | 37 return d.CreatedAt.Add(time.Duration(d.ExpiresIn) * time.Second).Before(time.Now()) |
| paddy@0 | 38 } |
| paddy@0 | 39 |
| paddy@0 | 40 // ExpireAt returns the expiration date |
| paddy@0 | 41 func (d *AccessData) ExpireAt() time.Time { |
| paddy@0 | 42 return d.CreatedAt.Add(time.Duration(d.ExpiresIn) * time.Second) |
| paddy@0 | 43 } |
| paddy@0 | 44 |
| paddy@0 | 45 // HandleOAuth2AccessRequest is the http.HandlerFunc for handling access token requests. |
| paddy@0 | 46 func HandleOAuth2AccessRequest(w http.ResponseWriter, r *http.Request, ctx Context) { |
| paddy@0 | 47 // Only allow GET or POST |
| paddy@0 | 48 if r.Method != "POST" { |
| paddy@1 | 49 if r.Method != "GET" || !ctx.Config.AllowGetAccessRequest { |
| paddy@3 | 50 ctx.RenderJSONError(w, ErrorInvalidRequest, "Invalid request method.", ctx.Config.DocumentationDomain) |
| paddy@0 | 51 return |
| paddy@0 | 52 } |
| paddy@0 | 53 } |
| paddy@0 | 54 |
| paddy@0 | 55 grantType := GrantType(r.Form.Get("grant_type")) |
| paddy@0 | 56 if ctx.Config.AllowedAccessTypes.Exists(grantType) { |
| paddy@0 | 57 switch grantType { |
| paddy@0 | 58 case AuthorizationCodeGrant: |
| paddy@0 | 59 handleAuthorizationCodeRequest(w, r, ctx) |
| paddy@0 | 60 case RefreshTokenGrant: |
| paddy@0 | 61 handleRefreshTokenRequest(w, r, ctx) |
| paddy@0 | 62 case PasswordGrant: |
| paddy@0 | 63 handlePasswordRequest(w, r, ctx) |
| paddy@0 | 64 case ClientCredentialsGrant: |
| paddy@0 | 65 handleClientCredentialsRequest(w, r, ctx) |
| paddy@0 | 66 default: |
| paddy@3 | 67 ctx.RenderJSONError(w, ErrorUnsupportedGrantType, "Unsupported grant type.", ctx.Config.DocumentationDomain) |
| paddy@0 | 68 return |
| paddy@0 | 69 } |
| paddy@0 | 70 } |
| paddy@0 | 71 } |
| paddy@0 | 72 |
| paddy@0 | 73 func handleAuthorizationCodeRequest(w http.ResponseWriter, r *http.Request, ctx Context) { |
| paddy@0 | 74 // get client authentication |
| paddy@0 | 75 auth, err := getClientAuth(r, ctx.Config.AllowClientSecretInParams) |
| paddy@0 | 76 if err != nil { |
| paddy@3 | 77 ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain) |
| paddy@0 | 78 return |
| paddy@0 | 79 } |
| paddy@0 | 80 |
| paddy@1 | 81 code := r.Form.Get("code") |
| paddy@0 | 82 // "code" is required |
| paddy@1 | 83 if code == "" { |
| paddy@3 | 84 ctx.RenderJSONError(w, ErrorInvalidRequest, "Code must be supplied.", ctx.Config.DocumentationDomain) |
| paddy@0 | 85 return |
| paddy@0 | 86 } |
| paddy@0 | 87 |
| paddy@0 | 88 // must have a valid client |
| paddy@1 | 89 client, err := getClient(auth, ctx) |
| paddy@0 | 90 if err != nil { |
| paddy@5 | 91 if err == ClientNotFoundError || err == InvalidClientError { |
| paddy@5 | 92 ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain) |
| paddy@9 | 93 return |
| paddy@5 | 94 } |
| paddy@9 | 95 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain) |
| paddy@0 | 96 return |
| paddy@0 | 97 } |
| paddy@0 | 98 |
| paddy@0 | 99 // must be a valid authorization code |
| paddy@1 | 100 authData, err := ctx.Tokens.GetAuthorization(code) |
| paddy@0 | 101 if err != nil { |
| paddy@9 | 102 if err == AuthorizationNotFoundError { |
| paddy@9 | 103 ctx.RenderJSONError(w, ErrorInvalidGrant, "Invalid authorization.", ctx.Config.DocumentationDomain) |
| paddy@9 | 104 return |
| paddy@9 | 105 } |
| paddy@9 | 106 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain) |
| paddy@0 | 107 return |
| paddy@0 | 108 } |
| paddy@5 | 109 if authData.RedirectURI == "" { |
| paddy@5 | 110 ctx.RenderJSONError(w, ErrorInvalidGrant, "Invalid redirect on grant.", ctx.Config.DocumentationDomain) |
| paddy@0 | 111 return |
| paddy@5 | 112 } |
| paddy@1 | 113 if authData.IsExpired() { |
| paddy@3 | 114 ctx.RenderJSONError(w, ErrorInvalidGrant, "Authorization is expired.", ctx.Config.DocumentationDomain) |
| paddy@3 | 115 return |
| paddy@0 | 116 } |
| paddy@0 | 117 |
| paddy@0 | 118 // code must be from the client |
| paddy@1 | 119 if !authData.Client.ID.Equal(client.ID) { |
| paddy@3 | 120 ctx.RenderJSONError(w, ErrorInvalidGrant, "Grant issued to another client.", ctx.Config.DocumentationDomain) |
| paddy@0 | 121 return |
| paddy@0 | 122 } |
| paddy@0 | 123 |
| paddy@0 | 124 // check redirect uri |
| paddy@1 | 125 redirectURI := r.Form.Get("redirect_uri") |
| paddy@1 | 126 if redirectURI == "" { |
| paddy@1 | 127 redirectURI = client.RedirectURI |
| paddy@0 | 128 } |
| paddy@1 | 129 if err = validateURI(client.RedirectURI, redirectURI); err != nil { |
| paddy@3 | 130 ctx.RenderJSONError(w, ErrorInvalidGrant, "Redirect URI doesn't match client.", ctx.Config.DocumentationDomain) |
| paddy@0 | 131 return |
| paddy@0 | 132 } |
| paddy@1 | 133 if authData.RedirectURI != redirectURI { |
| paddy@3 | 134 ctx.RenderJSONError(w, ErrorInvalidGrant, "Redirect URI doesn't match auth redirect.", ctx.Config.DocumentationDomain) |
| paddy@0 | 135 return |
| paddy@0 | 136 } |
| paddy@0 | 137 |
| paddy@1 | 138 data := AccessData{ |
| paddy@1 | 139 AuthRequest: AuthRequest{ |
| paddy@1 | 140 Client: client, |
| paddy@1 | 141 RedirectURI: redirectURI, |
| paddy@1 | 142 Scope: authData.Scope, |
| paddy@1 | 143 }, |
| paddy@1 | 144 PreviousAuthorizeData: &authData, |
| paddy@1 | 145 } |
| paddy@1 | 146 |
| paddy@1 | 147 err = fillTokens(&data, true, ctx) |
| paddy@1 | 148 if err != nil { |
| paddy@7 | 149 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain) |
| paddy@1 | 150 return |
| paddy@1 | 151 } |
| paddy@4 | 152 ctx.RenderJSONToken(w, data) |
| paddy@0 | 153 } |
| paddy@0 | 154 |
| paddy@0 | 155 func handleRefreshTokenRequest(w http.ResponseWriter, r *http.Request, ctx Context) { |
| paddy@0 | 156 // get client authentication |
| paddy@0 | 157 auth, err := getClientAuth(r, ctx.Config.AllowClientSecretInParams) |
| paddy@3 | 158 |
| paddy@0 | 159 if err != nil { |
| paddy@3 | 160 ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain) |
| paddy@0 | 161 return |
| paddy@0 | 162 } |
| paddy@0 | 163 |
| paddy@1 | 164 code := r.Form.Get("refresh_token") |
| paddy@0 | 165 |
| paddy@0 | 166 // "refresh_token" is required |
| paddy@1 | 167 if code == "" { |
| paddy@3 | 168 ctx.RenderJSONError(w, ErrorInvalidRequest, "Missing refresh token.", ctx.Config.DocumentationDomain) |
| paddy@0 | 169 return |
| paddy@0 | 170 } |
| paddy@0 | 171 |
| paddy@0 | 172 // must have a valid client |
| paddy@1 | 173 client, err := getClient(auth, ctx) |
| paddy@0 | 174 if err != nil { |
| paddy@5 | 175 if err == ClientNotFoundError || err == InvalidClientError { |
| paddy@5 | 176 ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain) |
| paddy@9 | 177 return |
| paddy@5 | 178 } |
| paddy@9 | 179 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain) |
| paddy@0 | 180 return |
| paddy@0 | 181 } |
| paddy@0 | 182 |
| paddy@0 | 183 // must be a valid refresh code |
| paddy@1 | 184 refreshData, err := ctx.Tokens.GetRefresh(code) |
| paddy@0 | 185 if err != nil { |
| paddy@9 | 186 if err == TokenNotFoundError { |
| paddy@9 | 187 ctx.RenderJSONError(w, ErrorInvalidGrant, "Refresh token not valid.", ctx.Config.DocumentationDomain) |
| paddy@9 | 188 return |
| paddy@9 | 189 } |
| paddy@9 | 190 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain) |
| paddy@0 | 191 return |
| paddy@0 | 192 } |
| paddy@0 | 193 |
| paddy@0 | 194 // client must be the same as the previous token |
| paddy@1 | 195 if !refreshData.Client.ID.Equal(client.ID) { |
| paddy@3 | 196 ctx.RenderJSONError(w, ErrorInvalidGrant, "Refresh token issued to another client.", ctx.Config.DocumentationDomain) |
| paddy@0 | 197 return |
| paddy@0 | 198 } |
| paddy@0 | 199 |
| paddy@1 | 200 scope := r.Form.Get("scope") |
| paddy@1 | 201 if scope == "" { |
| paddy@1 | 202 scope = refreshData.Scope |
| paddy@0 | 203 } |
| paddy@0 | 204 |
| paddy@1 | 205 data := AccessData{ |
| paddy@1 | 206 AuthRequest: AuthRequest{ |
| paddy@5 | 207 Client: client, |
| paddy@5 | 208 Scope: scope, |
| paddy@1 | 209 }, |
| paddy@1 | 210 PreviousAccessData: &refreshData, |
| paddy@1 | 211 } |
| paddy@1 | 212 err = fillTokens(&data, true, ctx) |
| paddy@1 | 213 if err != nil { |
| paddy@7 | 214 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain) |
| paddy@1 | 215 return |
| paddy@1 | 216 } |
| paddy@4 | 217 ctx.RenderJSONToken(w, data) |
| paddy@0 | 218 } |
| paddy@0 | 219 |
| paddy@0 | 220 func handlePasswordRequest(w http.ResponseWriter, r *http.Request, ctx Context) { |
| paddy@0 | 221 // get client authentication |
| paddy@0 | 222 auth, err := getClientAuth(r, ctx.Config.AllowClientSecretInParams) |
| paddy@0 | 223 if err != nil { |
| paddy@4 | 224 ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain) |
| paddy@0 | 225 return |
| paddy@0 | 226 } |
| paddy@0 | 227 |
| paddy@1 | 228 username := r.Form.Get("username") |
| paddy@1 | 229 password := r.Form.Get("password") |
| paddy@1 | 230 scope := r.Form.Get("scope") |
| paddy@0 | 231 |
| paddy@0 | 232 // "username" and "password" is required |
| paddy@1 | 233 if username == "" || password == "" { |
| paddy@4 | 234 ctx.RenderJSONError(w, ErrorInvalidRequest, "Missing credentials.", ctx.Config.DocumentationDomain) |
| paddy@0 | 235 return |
| paddy@0 | 236 } |
| paddy@0 | 237 |
| paddy@0 | 238 // must have a valid client |
| paddy@1 | 239 client, err := getClient(auth, ctx) |
| paddy@0 | 240 if err != nil { |
| paddy@5 | 241 if err == ClientNotFoundError || err == InvalidClientError { |
| paddy@5 | 242 ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain) |
| paddy@9 | 243 return |
| paddy@5 | 244 } |
| paddy@9 | 245 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain) |
| paddy@0 | 246 return |
| paddy@0 | 247 } |
| paddy@0 | 248 |
| paddy@1 | 249 _, err = ctx.Profiles.GetProfile(username, password) |
| paddy@1 | 250 if err != nil { |
| paddy@9 | 251 if err == ProfileNotFoundError { |
| paddy@9 | 252 ctx.RenderJSONError(w, ErrorInvalidGrant, "Invalid credentials.", ctx.Config.DocumentationDomain) |
| paddy@9 | 253 return |
| paddy@9 | 254 } |
| paddy@9 | 255 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain) |
| paddy@1 | 256 return |
| paddy@1 | 257 } |
| paddy@1 | 258 |
| paddy@1 | 259 data := AccessData{ |
| paddy@1 | 260 AuthRequest: AuthRequest{ |
| paddy@5 | 261 Client: client, |
| paddy@5 | 262 Scope: scope, |
| paddy@1 | 263 }, |
| paddy@1 | 264 } |
| paddy@1 | 265 |
| paddy@1 | 266 err = fillTokens(&data, true, ctx) |
| paddy@1 | 267 if err != nil { |
| paddy@7 | 268 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain) |
| paddy@1 | 269 return |
| paddy@1 | 270 } |
| paddy@4 | 271 ctx.RenderJSONToken(w, data) |
| paddy@0 | 272 } |
| paddy@0 | 273 |
| paddy@0 | 274 func handleClientCredentialsRequest(w http.ResponseWriter, r *http.Request, ctx Context) { |
| paddy@0 | 275 // get client authentication |
| paddy@0 | 276 auth, err := getClientAuth(r, ctx.Config.AllowClientSecretInParams) |
| paddy@0 | 277 if err != nil { |
| paddy@4 | 278 ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain) |
| paddy@0 | 279 return |
| paddy@0 | 280 } |
| paddy@0 | 281 |
| paddy@1 | 282 scope := r.Form.Get("scope") |
| paddy@0 | 283 |
| paddy@0 | 284 // must have a valid client |
| paddy@1 | 285 client, err := getClient(auth, ctx) |
| paddy@0 | 286 if err != nil { |
| paddy@5 | 287 if err == ClientNotFoundError || err == InvalidClientError { |
| paddy@5 | 288 ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain) |
| paddy@9 | 289 return |
| paddy@5 | 290 } |
| paddy@9 | 291 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain) |
| paddy@0 | 292 return |
| paddy@0 | 293 } |
| paddy@0 | 294 |
| paddy@1 | 295 data := AccessData{ |
| paddy@1 | 296 AuthRequest: AuthRequest{ |
| paddy@5 | 297 Client: client, |
| paddy@5 | 298 Scope: scope, |
| paddy@1 | 299 }, |
| paddy@1 | 300 } |
| paddy@0 | 301 |
| paddy@1 | 302 err = fillTokens(&data, true, ctx) |
| paddy@0 | 303 if err != nil { |
| paddy@7 | 304 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain) |
| paddy@0 | 305 return |
| paddy@0 | 306 } |
| paddy@4 | 307 ctx.RenderJSONToken(w, data) |
| paddy@0 | 308 } |
| paddy@0 | 309 |
| paddy@1 | 310 func fillTokens(data *AccessData, includeRefresh bool, ctx Context) error { |
| paddy@0 | 311 var err error |
| paddy@0 | 312 |
| paddy@0 | 313 // generate access token |
| paddy@1 | 314 data.AccessToken = newToken() |
| paddy@1 | 315 if includeRefresh { |
| paddy@1 | 316 data.RefreshToken = newToken() |
| paddy@0 | 317 } |
| paddy@0 | 318 |
| paddy@0 | 319 // save access token |
| paddy@1 | 320 err = ctx.Tokens.SaveAccess(*data) |
| paddy@0 | 321 if err != nil { |
| paddy@10 | 322 if ctx.Log != nil { |
| paddy@10 | 323 ctx.Log.Printf("Error writing access token: %s\n", err) |
| paddy@10 | 324 } |
| paddy@7 | 325 return InternalServerError |
| paddy@0 | 326 } |
| paddy@0 | 327 |
| paddy@0 | 328 // remove authorization token |
| paddy@1 | 329 if data.PreviousAuthorizeData != nil { |
| paddy@1 | 330 err = ctx.Tokens.RemoveAuthorization(data.PreviousAuthorizeData.Code) |
| paddy@10 | 331 if err != nil && ctx.Log != nil { |
| paddy@10 | 332 ctx.Log.Printf("Error removing previous auth data (%s): %s\n", data.PreviousAuthorizeData.Code, err) |
| paddy@0 | 333 } |
| paddy@0 | 334 } |
| paddy@0 | 335 |
| paddy@0 | 336 // remove previous access token |
| paddy@1 | 337 if data.PreviousAccessData != nil { |
| paddy@1 | 338 if data.PreviousAccessData.RefreshToken != "" { |
| paddy@1 | 339 err = ctx.Tokens.RemoveRefresh(data.PreviousAccessData.RefreshToken) |
| paddy@10 | 340 if err != nil && ctx.Log != nil { |
| paddy@10 | 341 ctx.Log.Printf("Error removing previous refresh token (%s): %s\n", data.PreviousAccessData.RefreshToken, err) |
| paddy@0 | 342 } |
| paddy@0 | 343 } |
| paddy@1 | 344 err = ctx.Tokens.RemoveAccess(data.PreviousAccessData.AccessToken) |
| paddy@10 | 345 if err != nil && ctx.Log != nil { |
| paddy@10 | 346 ctx.Log.Printf("Error removing previous access token (%s): %s\n", data.PreviousAccessData.AccessToken, err) |
| paddy@0 | 347 } |
| paddy@0 | 348 } |
| paddy@0 | 349 |
| paddy@1 | 350 data.TokenType = ctx.Config.TokenType |
| paddy@1 | 351 data.ExpiresIn = ctx.Config.AccessExpiration |
| paddy@1 | 352 data.CreatedAt = time.Now() |
| paddy@1 | 353 return nil |
| paddy@0 | 354 } |
| paddy@0 | 355 |
| paddy@1 | 356 func (data AccessData) GetRedirect(fragment bool) (string, error) { |
| paddy@1 | 357 u, err := url.Parse(data.RedirectURI) |
| paddy@1 | 358 if err != nil { |
| paddy@1 | 359 return "", err |
| paddy@1 | 360 } |
| paddy@1 | 361 |
| paddy@1 | 362 // add parameters |
| paddy@1 | 363 q := u.Query() |
| paddy@1 | 364 q.Set("access_token", data.AccessToken) |
| paddy@1 | 365 q.Set("token_type", data.TokenType) |
| paddy@1 | 366 q.Set("expires_in", strconv.FormatInt(int64(data.ExpiresIn), 10)) |
| paddy@1 | 367 if data.RefreshToken != "" { |
| paddy@1 | 368 q.Set("refresh_token", data.RefreshToken) |
| paddy@1 | 369 } |
| paddy@1 | 370 if data.Scope != "" { |
| paddy@1 | 371 q.Set("scope", data.Scope) |
| paddy@1 | 372 } |
| paddy@1 | 373 if len(data.ProfileID) > 0 { |
| paddy@1 | 374 q.Set("profile", data.ProfileID.String()) |
| paddy@1 | 375 } |
| paddy@1 | 376 if fragment { |
| paddy@1 | 377 u.RawQuery = "" |
| paddy@1 | 378 u.Fragment = q.Encode() |
| paddy@1 | 379 } else { |
| paddy@1 | 380 u.RawQuery = q.Encode() |
| paddy@1 | 381 } |
| paddy@1 | 382 |
| paddy@1 | 383 return u.String(), nil |
| paddy@1 | 384 } |
| paddy@0 | 385 |
| paddy@0 | 386 // getClient looks up and authenticates the basic auth using the given |
| paddy@0 | 387 // storage. Sets an error on the response if auth fails or a server error occurs. |
| paddy@0 | 388 func getClient(auth BasicAuth, ctx Context) (Client, error) { |
| paddy@0 | 389 id, err := uuid.Parse(auth.Username) |
| paddy@0 | 390 if err != nil { |
| paddy@0 | 391 return Client{}, err |
| paddy@0 | 392 } |
| paddy@1 | 393 client, err := ctx.Clients.GetClient(id) |
| paddy@0 | 394 if err != nil { |
| paddy@5 | 395 if err == ClientNotFoundError { |
| paddy@5 | 396 return Client{}, err |
| paddy@5 | 397 } |
| paddy@10 | 398 if ctx.Log != nil { |
| paddy@10 | 399 ctx.Log.Printf("Error retrieving client %s: %s", id, err) |
| paddy@10 | 400 } |
| paddy@5 | 401 return Client{}, InternalServerError |
| paddy@0 | 402 } |
| paddy@0 | 403 if client.Secret != auth.Password { |
| paddy@5 | 404 return Client{}, InvalidClientError |
| paddy@0 | 405 } |
| paddy@0 | 406 if client.RedirectURI == "" { |
| paddy@5 | 407 return Client{}, InvalidClientError |
| paddy@0 | 408 } |
| paddy@0 | 409 return client, nil |
| paddy@0 | 410 } |