276 lines
7.2 KiB
Go
276 lines
7.2 KiB
Go
|
package osin
|
||
|
|
||
|
import (
|
||
|
"net/http"
|
||
|
"net/url"
|
||
|
"regexp"
|
||
|
"time"
|
||
|
)
|
||
|
|
||
|
// AuthorizeRequestType is the type for OAuth param `response_type`
|
||
|
type AuthorizeRequestType string
|
||
|
|
||
|
const (
|
||
|
CODE AuthorizeRequestType = "code"
|
||
|
TOKEN AuthorizeRequestType = "token"
|
||
|
|
||
|
PKCE_PLAIN = "plain"
|
||
|
PKCE_S256 = "S256"
|
||
|
)
|
||
|
|
||
|
var (
|
||
|
pkceMatcher = regexp.MustCompile("^[a-zA-Z0-9~._-]{43,128}$")
|
||
|
)
|
||
|
|
||
|
// Authorize request information
|
||
|
type AuthorizeRequest struct {
|
||
|
Type AuthorizeRequestType
|
||
|
Client Client
|
||
|
Scope string
|
||
|
RedirectUri string
|
||
|
State string
|
||
|
|
||
|
// Set if request is authorized
|
||
|
Authorized bool
|
||
|
|
||
|
// Token expiration in seconds. Change if different from default.
|
||
|
// If type = TOKEN, this expiration will be for the ACCESS token.
|
||
|
Expiration int32
|
||
|
|
||
|
// Data to be passed to storage. Not used by the library.
|
||
|
UserData interface{}
|
||
|
|
||
|
// HttpRequest *http.Request for special use
|
||
|
HttpRequest *http.Request
|
||
|
|
||
|
// Optional code_challenge as described in rfc7636
|
||
|
CodeChallenge string
|
||
|
// Optional code_challenge_method as described in rfc7636
|
||
|
CodeChallengeMethod string
|
||
|
}
|
||
|
|
||
|
// Authorization data
|
||
|
type AuthorizeData struct {
|
||
|
// Client information
|
||
|
Client Client
|
||
|
|
||
|
// Authorization code
|
||
|
Code string
|
||
|
|
||
|
// Token expiration in seconds
|
||
|
ExpiresIn int32
|
||
|
|
||
|
// Requested scope
|
||
|
Scope string
|
||
|
|
||
|
// Redirect Uri from request
|
||
|
RedirectUri string
|
||
|
|
||
|
// State data from request
|
||
|
State string
|
||
|
|
||
|
// Date created
|
||
|
CreatedAt time.Time
|
||
|
|
||
|
// Data to be passed to storage. Not used by the library.
|
||
|
UserData interface{}
|
||
|
|
||
|
// Optional code_challenge as described in rfc7636
|
||
|
CodeChallenge string
|
||
|
// Optional code_challenge_method as described in rfc7636
|
||
|
CodeChallengeMethod string
|
||
|
}
|
||
|
|
||
|
// IsExpired is true if authorization expired
|
||
|
func (d *AuthorizeData) IsExpired() bool {
|
||
|
return d.IsExpiredAt(time.Now())
|
||
|
}
|
||
|
|
||
|
// IsExpired is true if authorization expires at time 't'
|
||
|
func (d *AuthorizeData) IsExpiredAt(t time.Time) bool {
|
||
|
return d.ExpireAt().Before(t)
|
||
|
}
|
||
|
|
||
|
// ExpireAt returns the expiration date
|
||
|
func (d *AuthorizeData) ExpireAt() time.Time {
|
||
|
return d.CreatedAt.Add(time.Duration(d.ExpiresIn) * time.Second)
|
||
|
}
|
||
|
|
||
|
// AuthorizeTokenGen is the token generator interface
|
||
|
type AuthorizeTokenGen interface {
|
||
|
GenerateAuthorizeToken(data *AuthorizeData) (string, error)
|
||
|
}
|
||
|
|
||
|
// HandleAuthorizeRequest is the main http.HandlerFunc for handling
|
||
|
// authorization requests
|
||
|
func (s *Server) HandleAuthorizeRequest(w *Response, r *http.Request) *AuthorizeRequest {
|
||
|
r.ParseForm()
|
||
|
|
||
|
// create the authorization request
|
||
|
unescapedUri, err := url.QueryUnescape(r.Form.Get("redirect_uri"))
|
||
|
if err != nil {
|
||
|
w.SetErrorState(E_INVALID_REQUEST, "", "")
|
||
|
w.InternalError = err
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
ret := &AuthorizeRequest{
|
||
|
State: r.Form.Get("state"),
|
||
|
Scope: r.Form.Get("scope"),
|
||
|
RedirectUri: unescapedUri,
|
||
|
Authorized: false,
|
||
|
HttpRequest: r,
|
||
|
}
|
||
|
|
||
|
// must have a valid client
|
||
|
ret.Client, err = w.Storage.GetClient(r.Form.Get("client_id"))
|
||
|
if err == ErrNotFound {
|
||
|
w.SetErrorState(E_UNAUTHORIZED_CLIENT, "", ret.State)
|
||
|
return nil
|
||
|
}
|
||
|
if err != nil {
|
||
|
w.SetErrorState(E_SERVER_ERROR, "", ret.State)
|
||
|
w.InternalError = err
|
||
|
return nil
|
||
|
}
|
||
|
if ret.Client == nil {
|
||
|
w.SetErrorState(E_UNAUTHORIZED_CLIENT, "", ret.State)
|
||
|
return nil
|
||
|
}
|
||
|
if ret.Client.GetRedirectUri() == "" {
|
||
|
w.SetErrorState(E_UNAUTHORIZED_CLIENT, "", ret.State)
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// check redirect uri, if there are multiple client redirect uri's
|
||
|
// don't set the uri
|
||
|
if ret.RedirectUri == "" && FirstUri(ret.Client.GetRedirectUri(), s.Config.RedirectUriSeparator) == ret.Client.GetRedirectUri() {
|
||
|
ret.RedirectUri = FirstUri(ret.Client.GetRedirectUri(), s.Config.RedirectUriSeparator)
|
||
|
}
|
||
|
|
||
|
if err = ValidateUriList(ret.Client.GetRedirectUri(), ret.RedirectUri, s.Config.RedirectUriSeparator); err != nil {
|
||
|
w.SetErrorState(E_INVALID_REQUEST, "", ret.State)
|
||
|
w.InternalError = err
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
w.SetRedirect(ret.RedirectUri)
|
||
|
|
||
|
requestType := AuthorizeRequestType(r.Form.Get("response_type"))
|
||
|
if s.Config.AllowedAuthorizeTypes.Exists(requestType) {
|
||
|
switch requestType {
|
||
|
case CODE:
|
||
|
ret.Type = CODE
|
||
|
ret.Expiration = s.Config.AuthorizationExpiration
|
||
|
|
||
|
// Optional PKCE support (https://tools.ietf.org/html/rfc7636)
|
||
|
if codeChallenge := r.Form.Get("code_challenge"); len(codeChallenge) == 0 {
|
||
|
if s.Config.RequirePKCEForPublicClients && CheckClientSecret(ret.Client, "") {
|
||
|
// https://tools.ietf.org/html/rfc7636#section-4.4.1
|
||
|
w.SetErrorState(E_INVALID_REQUEST, "code_challenge (rfc7636) required for public clients", ret.State)
|
||
|
return nil
|
||
|
}
|
||
|
} else {
|
||
|
codeChallengeMethod := r.Form.Get("code_challenge_method")
|
||
|
// allowed values are "plain" (default) and "S256", per https://tools.ietf.org/html/rfc7636#section-4.3
|
||
|
if len(codeChallengeMethod) == 0 {
|
||
|
codeChallengeMethod = PKCE_PLAIN
|
||
|
}
|
||
|
if codeChallengeMethod != PKCE_PLAIN && codeChallengeMethod != PKCE_S256 {
|
||
|
// https://tools.ietf.org/html/rfc7636#section-4.4.1
|
||
|
w.SetErrorState(E_INVALID_REQUEST, "code_challenge_method transform algorithm not supported (rfc7636)", ret.State)
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// https://tools.ietf.org/html/rfc7636#section-4.2
|
||
|
if matched := pkceMatcher.MatchString(codeChallenge); !matched {
|
||
|
w.SetErrorState(E_INVALID_REQUEST, "code_challenge invalid (rfc7636)", ret.State)
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
ret.CodeChallenge = codeChallenge
|
||
|
ret.CodeChallengeMethod = codeChallengeMethod
|
||
|
}
|
||
|
|
||
|
case TOKEN:
|
||
|
ret.Type = TOKEN
|
||
|
ret.Expiration = s.Config.AccessExpiration
|
||
|
}
|
||
|
return ret
|
||
|
}
|
||
|
|
||
|
w.SetErrorState(E_UNSUPPORTED_RESPONSE_TYPE, "", ret.State)
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (s *Server) FinishAuthorizeRequest(w *Response, r *http.Request, ar *AuthorizeRequest) {
|
||
|
// don't process if is already an error
|
||
|
if w.IsError {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// force redirect response
|
||
|
w.SetRedirect(ar.RedirectUri)
|
||
|
|
||
|
if ar.Authorized {
|
||
|
if ar.Type == TOKEN {
|
||
|
w.SetRedirectFragment(true)
|
||
|
|
||
|
// generate token directly
|
||
|
ret := &AccessRequest{
|
||
|
Type: IMPLICIT,
|
||
|
Code: "",
|
||
|
Client: ar.Client,
|
||
|
RedirectUri: ar.RedirectUri,
|
||
|
Scope: ar.Scope,
|
||
|
GenerateRefresh: false, // per the RFC, should NOT generate a refresh token in this case
|
||
|
Authorized: true,
|
||
|
Expiration: ar.Expiration,
|
||
|
UserData: ar.UserData,
|
||
|
}
|
||
|
|
||
|
s.FinishAccessRequest(w, r, ret)
|
||
|
if ar.State != "" && w.InternalError == nil {
|
||
|
w.Output["state"] = ar.State
|
||
|
}
|
||
|
} else {
|
||
|
// generate authorization token
|
||
|
ret := &AuthorizeData{
|
||
|
Client: ar.Client,
|
||
|
CreatedAt: s.Now(),
|
||
|
ExpiresIn: ar.Expiration,
|
||
|
RedirectUri: ar.RedirectUri,
|
||
|
State: ar.State,
|
||
|
Scope: ar.Scope,
|
||
|
UserData: ar.UserData,
|
||
|
// Optional PKCE challenge
|
||
|
CodeChallenge: ar.CodeChallenge,
|
||
|
CodeChallengeMethod: ar.CodeChallengeMethod,
|
||
|
}
|
||
|
|
||
|
// generate token code
|
||
|
code, err := s.AuthorizeTokenGen.GenerateAuthorizeToken(ret)
|
||
|
if err != nil {
|
||
|
w.SetErrorState(E_SERVER_ERROR, "", ar.State)
|
||
|
w.InternalError = err
|
||
|
return
|
||
|
}
|
||
|
ret.Code = code
|
||
|
|
||
|
// save authorization token
|
||
|
if err = w.Storage.SaveAuthorize(ret); err != nil {
|
||
|
w.SetErrorState(E_SERVER_ERROR, "", ar.State)
|
||
|
w.InternalError = err
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// redirect with code
|
||
|
w.Output["code"] = ret.Code
|
||
|
w.Output["state"] = ret.State
|
||
|
}
|
||
|
} else {
|
||
|
// redirect with error
|
||
|
w.SetErrorState(E_ACCESS_DENIED, "", ar.State)
|
||
|
}
|
||
|
}
|