begin refresh token
This commit is contained in:
parent
a2601f1584
commit
5119d7aea3
15 changed files with 611 additions and 275 deletions
|
@ -89,7 +89,7 @@ func Authorize(w http.ResponseWriter, r *http.Request, authorizer Authorizer) {
|
|||
RedirectToLogin(req.GetID(), client, w, r)
|
||||
}
|
||||
|
||||
//ParseAuthorizeRequest parsed the http request into a AuthRequest
|
||||
//ParseAuthorizeRequest parsed the http request into a oidc.AuthRequest
|
||||
func ParseAuthorizeRequest(r *http.Request, decoder utils.Decoder) (*oidc.AuthRequest, error) {
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
|
@ -289,6 +289,7 @@ func CreateAuthRequestCode(ctx context.Context, authReq AuthRequest, storage Sto
|
|||
return code, nil
|
||||
}
|
||||
|
||||
//BuildAuthRequestCode builds the string representation of the auth code
|
||||
func BuildAuthRequestCode(authReq AuthRequest, crypto Crypto) (string, error) {
|
||||
return crypto.Encrypt(authReq.GetID())
|
||||
}
|
|
@ -17,6 +17,8 @@ type AuthStorage interface {
|
|||
DeleteAuthRequest(context.Context, string) error
|
||||
|
||||
CreateToken(context.Context, TokenRequest) (string, time.Time, error)
|
||||
CreateTokens(ctx context.Context, request TokenRequest, currentRefreshToken string) (accessTokenID string, newRefreshToken string, expiration time.Time, err error)
|
||||
RefreshTokenRequestByRefreshToken(context.Context, string) (RefreshTokenRequest, error)
|
||||
|
||||
TerminateSession(context.Context, string, string) error
|
||||
|
||||
|
|
|
@ -21,53 +21,61 @@ type TokenRequest interface {
|
|||
GetScopes() []string
|
||||
}
|
||||
|
||||
func CreateTokenResponse(ctx context.Context, authReq AuthRequest, client Client, creator TokenCreator, createAccessToken bool, code string) (*oidc.AccessTokenResponse, error) {
|
||||
var accessToken string
|
||||
func CreateTokenResponse(ctx context.Context, request IDTokenRequest, client Client, creator TokenCreator, createAccessToken bool, code string) (*oidc.AccessTokenResponse, error) {
|
||||
var accessToken, refreshToken string
|
||||
var validity time.Duration
|
||||
if createAccessToken {
|
||||
var err error
|
||||
accessToken, validity, err = CreateAccessToken(ctx, authReq, client.AccessTokenType(), creator, client)
|
||||
accessToken, refreshToken, validity, err = CreateAccessToken(ctx, request, client.AccessTokenType(), creator, client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
idToken, err := CreateIDToken(ctx, creator.Issuer(), authReq, client.IDTokenLifetime(), accessToken, code, creator.Storage(), creator.Signer(), client)
|
||||
idToken, err := CreateIDToken(ctx, creator.Issuer(), request, client.IDTokenLifetime(), accessToken, code, creator.Storage(), creator.Signer(), client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = creator.Storage().DeleteAuthRequest(ctx, authReq.GetID())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if authRequest, ok := request.(AuthRequest); ok {
|
||||
err = creator.Storage().DeleteAuthRequest(ctx, authRequest.GetID())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
exp := uint64(validity.Seconds())
|
||||
return &oidc.AccessTokenResponse{
|
||||
AccessToken: accessToken,
|
||||
IDToken: idToken,
|
||||
TokenType: oidc.BearerToken,
|
||||
ExpiresIn: exp,
|
||||
AccessToken: accessToken,
|
||||
IDToken: idToken,
|
||||
RefreshToken: refreshToken,
|
||||
TokenType: oidc.BearerToken,
|
||||
ExpiresIn: exp,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func CreateJWTTokenResponse(ctx context.Context, tokenRequest TokenRequest, creator TokenCreator) (*oidc.AccessTokenResponse, error) {
|
||||
accessToken, validity, err := CreateAccessToken(ctx, tokenRequest, AccessTokenTypeBearer, creator, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
func createTokens(ctx context.Context, tokenRequest TokenRequest, storage Storage) (id, refreshToken string, exp time.Time, err error) {
|
||||
if needsRefreshToken(tokenRequest) {
|
||||
id, exp, err = storage.CreateToken(ctx, tokenRequest)
|
||||
return
|
||||
}
|
||||
|
||||
exp := uint64(validity.Seconds())
|
||||
return &oidc.AccessTokenResponse{
|
||||
AccessToken: accessToken,
|
||||
TokenType: oidc.BearerToken,
|
||||
ExpiresIn: exp,
|
||||
}, nil
|
||||
return storage.CreateTokens(ctx, tokenRequest, "hodor")
|
||||
}
|
||||
|
||||
func CreateAccessToken(ctx context.Context, tokenRequest TokenRequest, accessTokenType AccessTokenType, creator TokenCreator, client Client) (token string, validity time.Duration, err error) {
|
||||
id, exp, err := creator.Storage().CreateToken(ctx, tokenRequest)
|
||||
func needsRefreshToken(tokenRequest TokenRequest) bool {
|
||||
switch req := tokenRequest.(type) {
|
||||
case AuthRequest:
|
||||
return utils.Contains(req.GetScopes(), oidc.ScopeOfflineAccess) && req.GetResponseType() == oidc.ResponseTypeCode
|
||||
case RefreshTokenRequest:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func CreateAccessToken(ctx context.Context, tokenRequest TokenRequest, accessTokenType AccessTokenType, creator TokenCreator, client Client) (accessToken, refreshToken string, validity time.Duration, err error) {
|
||||
id, refreshToken, exp, err := createTokens(ctx, tokenRequest, creator.Storage())
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
return "", "", 0, err
|
||||
}
|
||||
var clockSkew time.Duration
|
||||
if client != nil {
|
||||
|
@ -75,10 +83,10 @@ func CreateAccessToken(ctx context.Context, tokenRequest TokenRequest, accessTok
|
|||
}
|
||||
validity = exp.Add(clockSkew).Sub(time.Now().UTC())
|
||||
if accessTokenType == AccessTokenTypeJWT {
|
||||
token, err = CreateJWT(ctx, creator.Issuer(), tokenRequest, exp, id, creator.Signer(), client, creator.Storage())
|
||||
accessToken, err = CreateJWT(ctx, creator.Issuer(), tokenRequest, exp, id, creator.Signer(), client, creator.Storage())
|
||||
return
|
||||
}
|
||||
token, err = CreateBearerToken(id, tokenRequest.GetSubject(), creator.Crypto())
|
||||
accessToken, err = CreateBearerToken(id, tokenRequest.GetSubject(), creator.Crypto())
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -99,10 +107,27 @@ func CreateJWT(ctx context.Context, issuer string, tokenRequest TokenRequest, ex
|
|||
return utils.Sign(claims, signer.Signer())
|
||||
}
|
||||
|
||||
func CreateIDToken(ctx context.Context, issuer string, authReq AuthRequest, validity time.Duration, accessToken, code string, storage Storage, signer Signer, client Client) (string, error) {
|
||||
type IDTokenRequest interface {
|
||||
//GetACR() string
|
||||
//GetAMR() []string
|
||||
GetAudience() []string
|
||||
GetAuthTime() time.Time
|
||||
GetClientID() string
|
||||
GetScopes() []string
|
||||
GetSubject() string
|
||||
}
|
||||
|
||||
func CreateIDToken(ctx context.Context, issuer string, request IDTokenRequest, validity time.Duration, accessToken, code string, storage Storage, signer Signer, client Client) (string, error) {
|
||||
exp := time.Now().UTC().Add(client.ClockSkew()).Add(validity)
|
||||
claims := oidc.NewIDTokenClaims(issuer, authReq.GetSubject(), authReq.GetAudience(), exp, authReq.GetAuthTime(), authReq.GetNonce(), authReq.GetACR(), authReq.GetAMR(), authReq.GetClientID(), client.ClockSkew())
|
||||
scopes := client.RestrictAdditionalIdTokenScopes()(authReq.GetScopes())
|
||||
var acr, nonce string
|
||||
var amr []string
|
||||
if authRequest, ok := request.(AuthRequest); ok {
|
||||
acr = authRequest.GetACR()
|
||||
amr = authRequest.GetAMR()
|
||||
nonce = authRequest.GetNonce()
|
||||
}
|
||||
claims := oidc.NewIDTokenClaims(issuer, request.GetSubject(), request.GetAudience(), exp, request.GetAuthTime(), nonce, acr, amr, request.GetClientID(), client.ClockSkew())
|
||||
scopes := client.RestrictAdditionalIdTokenScopes()(request.GetScopes())
|
||||
if accessToken != "" {
|
||||
atHash, err := oidc.ClaimHash(accessToken, signer.SignatureAlgorithm())
|
||||
if err != nil {
|
||||
|
@ -115,7 +140,7 @@ func CreateIDToken(ctx context.Context, issuer string, authReq AuthRequest, vali
|
|||
}
|
||||
if len(scopes) > 0 {
|
||||
userInfo := oidc.NewUserInfo()
|
||||
err := storage.SetUserinfoFromScopes(ctx, userInfo, authReq.GetSubject(), authReq.GetClientID(), scopes)
|
||||
err := storage.SetUserinfoFromScopes(ctx, userInfo, request.GetSubject(), request.GetClientID(), scopes)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
107
pkg/op/token_code.go
Normal file
107
pkg/op/token_code.go
Normal file
|
@ -0,0 +1,107 @@
|
|||
package op
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/caos/oidc/pkg/oidc"
|
||||
"github.com/caos/oidc/pkg/utils"
|
||||
)
|
||||
|
||||
//CodeExchange handles the OAuth 2.0 authorization_code grant, including
|
||||
//parsing, validating, authorizing the client and finally exchanging the code for tokens
|
||||
func CodeExchange(w http.ResponseWriter, r *http.Request, exchanger Exchanger) {
|
||||
tokenReq, err := ParseAccessTokenRequest(r, exchanger.Decoder())
|
||||
if err != nil {
|
||||
RequestError(w, r, err)
|
||||
}
|
||||
if tokenReq.Code == "" {
|
||||
RequestError(w, r, ErrInvalidRequest("code missing"))
|
||||
return
|
||||
}
|
||||
authReq, client, err := ValidateAccessTokenRequest(r.Context(), tokenReq, exchanger)
|
||||
if err != nil {
|
||||
RequestError(w, r, err)
|
||||
return
|
||||
}
|
||||
resp, err := CreateTokenResponse(r.Context(), authReq, client, exchanger, true, tokenReq.Code)
|
||||
if err != nil {
|
||||
RequestError(w, r, err)
|
||||
return
|
||||
}
|
||||
utils.MarshalJSON(w, resp)
|
||||
}
|
||||
|
||||
//ParseAccessTokenRequest parsed the http request into a oidc.AccessTokenRequest
|
||||
func ParseAccessTokenRequest(r *http.Request, decoder utils.Decoder) (*oidc.AccessTokenRequest, error) {
|
||||
request := new(oidc.AccessTokenRequest)
|
||||
err := ParseAuthenticatedTokenRequest(r, decoder, request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return request, nil
|
||||
}
|
||||
|
||||
//ValidateAccessTokenRequest validates the token request parameters including authorization check of the client
|
||||
//and returns the previous created auth request corresponding to the auth code
|
||||
func ValidateAccessTokenRequest(ctx context.Context, tokenReq *oidc.AccessTokenRequest, exchanger Exchanger) (AuthRequest, Client, error) {
|
||||
authReq, client, err := AuthorizeCodeClient(ctx, tokenReq, exchanger)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if client.GetID() != authReq.GetClientID() {
|
||||
return nil, nil, ErrInvalidRequest("invalid auth code")
|
||||
}
|
||||
if tokenReq.RedirectURI != authReq.GetRedirectURI() {
|
||||
return nil, nil, ErrInvalidRequest("redirect_uri does no correspond")
|
||||
}
|
||||
return authReq, client, nil
|
||||
}
|
||||
|
||||
//AuthorizeCodeClient checks the authorization of the client and that the used method was the one previously registered.
|
||||
//It than returns the auth request corresponding to the auth code
|
||||
func AuthorizeCodeClient(ctx context.Context, tokenReq *oidc.AccessTokenRequest, exchanger Exchanger) (request AuthRequest, client Client, err error) {
|
||||
if tokenReq.ClientAssertionType == oidc.ClientAssertionTypeJWTAssertion {
|
||||
jwtExchanger, ok := exchanger.(JWTAuthorizationGrantExchanger)
|
||||
if !ok || !exchanger.AuthMethodPrivateKeyJWTSupported() {
|
||||
return nil, nil, errors.New("auth_method private_key_jwt not supported")
|
||||
}
|
||||
client, err = AuthorizePrivateJWTKey(ctx, tokenReq.ClientAssertion, jwtExchanger)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
request, err = AuthRequestByCode(ctx, exchanger.Storage(), tokenReq.Code)
|
||||
return request, client, err
|
||||
}
|
||||
client, err = exchanger.Storage().GetClientByClientID(ctx, tokenReq.ClientID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if client.AuthMethod() == oidc.AuthMethodPrivateKeyJWT {
|
||||
return nil, nil, errors.New("invalid_grant")
|
||||
}
|
||||
if client.AuthMethod() == oidc.AuthMethodNone {
|
||||
request, err = AuthRequestByCode(ctx, exchanger.Storage(), tokenReq.Code)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
err = AuthorizeCodeChallenge(tokenReq, request.GetCodeChallenge())
|
||||
return request, client, err
|
||||
}
|
||||
if client.AuthMethod() == oidc.AuthMethodPost && !exchanger.AuthMethodPostSupported() {
|
||||
return nil, nil, errors.New("auth_method post not supported")
|
||||
}
|
||||
err = AuthorizeClientIDSecret(ctx, tokenReq.ClientID, tokenReq.ClientSecret, exchanger.Storage())
|
||||
request, err = AuthRequestByCode(ctx, exchanger.Storage(), tokenReq.Code)
|
||||
return request, client, err
|
||||
}
|
||||
|
||||
//AuthRequestByCode returns the AuthRequest previously created from Storage corresponding to the auth code or an error
|
||||
func AuthRequestByCode(ctx context.Context, storage Storage, code string) (AuthRequest, error) {
|
||||
authReq, err := storage.AuthRequestByCode(ctx, code)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidRequest("invalid code")
|
||||
}
|
||||
return authReq, nil
|
||||
}
|
30
pkg/op/token_exchange.go
Normal file
30
pkg/op/token_exchange.go
Normal file
|
@ -0,0 +1,30 @@
|
|||
package op
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/caos/oidc/pkg/oidc"
|
||||
)
|
||||
|
||||
//TokenExchange will handle the OAuth 2.0 token exchange grant ("urn:ietf:params:oauth:grant-type:token-exchange")
|
||||
func TokenExchange(w http.ResponseWriter, r *http.Request, exchanger Exchanger) {
|
||||
tokenRequest, err := ParseTokenExchangeRequest(w, r)
|
||||
if err != nil {
|
||||
RequestError(w, r, err)
|
||||
return
|
||||
}
|
||||
err = ValidateTokenExchangeRequest(tokenRequest, exchanger.Storage())
|
||||
if err != nil {
|
||||
RequestError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func ParseTokenExchangeRequest(w http.ResponseWriter, r *http.Request) (oidc.TokenRequest, error) {
|
||||
return nil, errors.New("Unimplemented") //TODO: impl
|
||||
}
|
||||
|
||||
func ValidateTokenExchangeRequest(tokenReq oidc.TokenRequest, storage Storage) error {
|
||||
return errors.New("Unimplemented") //TODO: impl
|
||||
}
|
79
pkg/op/token_jwt_profile.go
Normal file
79
pkg/op/token_jwt_profile.go
Normal file
|
@ -0,0 +1,79 @@
|
|||
package op
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/caos/oidc/pkg/oidc"
|
||||
"github.com/caos/oidc/pkg/utils"
|
||||
)
|
||||
|
||||
type JWTAuthorizationGrantExchanger interface {
|
||||
Exchanger
|
||||
JWTProfileVerifier() JWTProfileVerifier
|
||||
}
|
||||
|
||||
//JWTProfile handles the OAuth 2.0 JWT Profile Authorization Grant https://tools.ietf.org/html/rfc7523#section-2.1
|
||||
func JWTProfile(w http.ResponseWriter, r *http.Request, exchanger JWTAuthorizationGrantExchanger) {
|
||||
profileRequest, err := ParseJWTProfileGrantRequest(r, exchanger.Decoder())
|
||||
if err != nil {
|
||||
RequestError(w, r, err)
|
||||
}
|
||||
|
||||
tokenRequest, err := VerifyJWTAssertion(r.Context(), profileRequest.Assertion, exchanger.JWTProfileVerifier())
|
||||
if err != nil {
|
||||
RequestError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
tokenRequest.Scopes, err = exchanger.Storage().ValidateJWTProfileScopes(r.Context(), tokenRequest.Issuer, profileRequest.Scope)
|
||||
if err != nil {
|
||||
RequestError(w, r, err)
|
||||
return
|
||||
}
|
||||
resp, err := CreateJWTTokenResponse(r.Context(), tokenRequest, exchanger)
|
||||
if err != nil {
|
||||
RequestError(w, r, err)
|
||||
return
|
||||
}
|
||||
utils.MarshalJSON(w, resp)
|
||||
}
|
||||
|
||||
func ParseJWTProfileGrantRequest(r *http.Request, decoder utils.Decoder) (*oidc.JWTProfileGrantRequest, error) {
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
return nil, ErrInvalidRequest("error parsing form")
|
||||
}
|
||||
tokenReq := new(oidc.JWTProfileGrantRequest)
|
||||
err = decoder.Decode(tokenReq, r.Form)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidRequest("error decoding form")
|
||||
}
|
||||
return tokenReq, nil
|
||||
}
|
||||
|
||||
//CreateJWTTokenResponse creates
|
||||
func CreateJWTTokenResponse(ctx context.Context, tokenRequest TokenRequest, creator TokenCreator) (*oidc.AccessTokenResponse, error) {
|
||||
id, exp, err := creator.Storage().CreateToken(ctx, tokenRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
accessToken, err := CreateBearerToken(id, tokenRequest.GetSubject(), creator.Crypto())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &oidc.AccessTokenResponse{
|
||||
AccessToken: accessToken,
|
||||
TokenType: oidc.BearerToken,
|
||||
ExpiresIn: uint64(exp.Sub(time.Now().UTC()).Seconds()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
//ParseJWTProfileRequest has been renamed to ParseJWTProfileGrantRequest
|
||||
//
|
||||
//deprecated: use ParseJWTProfileGrantRequest
|
||||
func ParseJWTProfileRequest(r *http.Request, decoder utils.Decoder) (*oidc.JWTProfileGrantRequest, error) {
|
||||
return ParseJWTProfileGrantRequest(r, decoder)
|
||||
}
|
113
pkg/op/token_refresh.go
Normal file
113
pkg/op/token_refresh.go
Normal file
|
@ -0,0 +1,113 @@
|
|||
package op
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/caos/oidc/pkg/oidc"
|
||||
"github.com/caos/oidc/pkg/utils"
|
||||
)
|
||||
|
||||
type RefreshTokenRequest interface {
|
||||
//GetID() string
|
||||
//GetACR() string
|
||||
//GetAMR() []string
|
||||
GetAudience() []string
|
||||
GetAuthTime() time.Time
|
||||
GetClientID() string
|
||||
GetScopes() []string
|
||||
GetSubject() string
|
||||
//GetRefreshToken() string
|
||||
}
|
||||
|
||||
//RefreshTokenExchange handles the OAuth 2.0 refresh_token grant, including
|
||||
//parsing, validating, authorizing the client and finally exchanging the refresh_token for new tokens
|
||||
func RefreshTokenExchange(w http.ResponseWriter, r *http.Request, exchanger Exchanger) {
|
||||
tokenReq, err := ParseRefreshTokenRequest(r, exchanger.Decoder())
|
||||
if err != nil {
|
||||
RequestError(w, r, err)
|
||||
}
|
||||
authReq, client, err := ValidateRefreshTokenRequest(r.Context(), tokenReq, exchanger)
|
||||
if err != nil {
|
||||
RequestError(w, r, err)
|
||||
return
|
||||
}
|
||||
resp, err := CreateTokenResponse(r.Context(), authReq, client, exchanger, true, "")
|
||||
if err != nil {
|
||||
RequestError(w, r, err)
|
||||
return
|
||||
}
|
||||
utils.MarshalJSON(w, resp)
|
||||
}
|
||||
|
||||
//ParseRefreshTokenRequest parsed the http request into a oidc.RefreshTokenRequest
|
||||
func ParseRefreshTokenRequest(r *http.Request, decoder utils.Decoder) (*oidc.RefreshTokenRequest, error) {
|
||||
request := new(oidc.RefreshTokenRequest)
|
||||
err := ParseAuthenticatedTokenRequest(r, decoder, request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return request, nil
|
||||
}
|
||||
|
||||
//ValidateRefreshTokenRequest validates the refresh_token request parameters including authorization check of the client
|
||||
//and returns the data representing the original auth request corresponding to the refresh_token
|
||||
func ValidateRefreshTokenRequest(ctx context.Context, tokenReq *oidc.RefreshTokenRequest, exchanger Exchanger) (RefreshTokenRequest, Client, error) {
|
||||
if tokenReq.RefreshToken == "" {
|
||||
return nil, nil, ErrInvalidRequest("code missing")
|
||||
}
|
||||
authReq, client, err := AuthorizeRefreshClient(ctx, tokenReq, exchanger)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if client.GetID() != authReq.GetClientID() {
|
||||
return nil, nil, ErrInvalidRequest("invalid auth code")
|
||||
}
|
||||
return authReq, client, nil
|
||||
}
|
||||
|
||||
//AuthorizeCodeClient checks the authorization of the client and that the used method was the one previously registered.
|
||||
//It than returns the data representing the original auth request corresponding to the refresh_token
|
||||
func AuthorizeRefreshClient(ctx context.Context, tokenReq *oidc.RefreshTokenRequest, exchanger Exchanger) (request RefreshTokenRequest, client Client, err error) {
|
||||
if tokenReq.ClientAssertionType == oidc.ClientAssertionTypeJWTAssertion {
|
||||
jwtExchanger, ok := exchanger.(JWTAuthorizationGrantExchanger)
|
||||
if !ok || !exchanger.AuthMethodPrivateKeyJWTSupported() {
|
||||
return nil, nil, errors.New("auth_method private_key_jwt not supported")
|
||||
}
|
||||
client, err = AuthorizePrivateJWTKey(ctx, tokenReq.ClientAssertion, jwtExchanger)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
request, err = RefreshTokenRequestByRefreshToken(ctx, exchanger.Storage(), tokenReq.RefreshToken)
|
||||
return request, client, err
|
||||
}
|
||||
client, err = exchanger.Storage().GetClientByClientID(ctx, tokenReq.ClientID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if client.AuthMethod() == oidc.AuthMethodPrivateKeyJWT {
|
||||
return nil, nil, errors.New("invalid_grant")
|
||||
}
|
||||
if client.AuthMethod() == oidc.AuthMethodNone {
|
||||
request, err = RefreshTokenRequestByRefreshToken(ctx, exchanger.Storage(), tokenReq.RefreshToken)
|
||||
return request, client, err
|
||||
}
|
||||
if client.AuthMethod() == oidc.AuthMethodPost && !exchanger.AuthMethodPostSupported() {
|
||||
return nil, nil, errors.New("auth_method post not supported")
|
||||
}
|
||||
err = AuthorizeClientIDSecret(ctx, tokenReq.ClientID, tokenReq.ClientSecret, exchanger.Storage())
|
||||
request, err = RefreshTokenRequestByRefreshToken(ctx, exchanger.Storage(), tokenReq.RefreshToken)
|
||||
return request, client, err
|
||||
}
|
||||
|
||||
//RefreshTokenRequestByRefreshToken returns the RefreshTokenRequest (data representing the original auth request)
|
||||
//corresponding to the refresh_token from Storage or an error
|
||||
func RefreshTokenRequestByRefreshToken(ctx context.Context, storage Storage, refreshToken string) (RefreshTokenRequest, error) {
|
||||
authReq, err := storage.RefreshTokenRequestByRefreshToken(ctx, refreshToken)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidRequest("invalid refreshToken")
|
||||
}
|
||||
return authReq, nil
|
||||
}
|
121
pkg/op/token_request.go
Normal file
121
pkg/op/token_request.go
Normal file
|
@ -0,0 +1,121 @@
|
|||
package op
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/caos/oidc/pkg/oidc"
|
||||
"github.com/caos/oidc/pkg/utils"
|
||||
)
|
||||
|
||||
type Exchanger interface {
|
||||
Issuer() string
|
||||
Storage() Storage
|
||||
Decoder() utils.Decoder
|
||||
Signer() Signer
|
||||
Crypto() Crypto
|
||||
AuthMethodPostSupported() bool
|
||||
AuthMethodPrivateKeyJWTSupported() bool
|
||||
GrantTypeTokenExchangeSupported() bool
|
||||
GrantTypeJWTAuthorizationSupported() bool
|
||||
}
|
||||
|
||||
func tokenHandler(exchanger Exchanger) func(w http.ResponseWriter, r *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.FormValue("grant_type") {
|
||||
case string(oidc.GrantTypeCode):
|
||||
CodeExchange(w, r, exchanger)
|
||||
return
|
||||
case string(oidc.GrantTypeRefreshToken):
|
||||
RefreshTokenExchange(w, r, exchanger)
|
||||
return
|
||||
case string(oidc.GrantTypeBearer):
|
||||
if ex, ok := exchanger.(JWTAuthorizationGrantExchanger); ok && exchanger.GrantTypeJWTAuthorizationSupported() {
|
||||
JWTProfile(w, r, ex)
|
||||
return
|
||||
}
|
||||
case string(oidc.GrantTypeTokenExchange):
|
||||
if exchanger.GrantTypeTokenExchangeSupported() {
|
||||
TokenExchange(w, r, exchanger)
|
||||
return
|
||||
}
|
||||
case "":
|
||||
RequestError(w, r, ErrInvalidRequest("grant_type missing"))
|
||||
return
|
||||
}
|
||||
RequestError(w, r, ErrInvalidRequest("grant_type not supported"))
|
||||
}
|
||||
}
|
||||
|
||||
//authenticatedTokenRequest is a helper interface for ParseAuthenticatedTokenRequest
|
||||
//it is implemented by oidc.AuthRequest and oidc.RefreshTokenRequest
|
||||
type AuthenticatedTokenRequest interface {
|
||||
SetClientID(string)
|
||||
SetClientSecret(string)
|
||||
}
|
||||
|
||||
//ParseAuthenticatedTokenRequest parses the client_id and client_secret from the HTTP request from either
|
||||
//HTTP Basic Auth header or form body and sets them into the provided authenticatedTokenRequest interface
|
||||
func ParseAuthenticatedTokenRequest(r *http.Request, decoder utils.Decoder, request AuthenticatedTokenRequest) error {
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
return ErrInvalidRequest("error parsing form")
|
||||
}
|
||||
err = decoder.Decode(request, r.Form)
|
||||
if err != nil {
|
||||
return ErrInvalidRequest("error decoding form")
|
||||
}
|
||||
clientID, clientSecret, ok := r.BasicAuth()
|
||||
if ok {
|
||||
clientID, err = url.QueryUnescape(clientID)
|
||||
if err != nil {
|
||||
return ErrInvalidRequest("invalid basic auth header")
|
||||
}
|
||||
clientSecret, err = url.QueryUnescape(clientSecret)
|
||||
if err != nil {
|
||||
return ErrInvalidRequest("invalid basic auth header")
|
||||
}
|
||||
request.SetClientID(clientID)
|
||||
request.SetClientSecret(clientSecret)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
//AuthorizeRefreshClientByClientIDSecret authorizes a client by validating the client_id and client_secret (Basic Auth and POST)
|
||||
func AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret string, storage Storage) error {
|
||||
err := storage.AuthorizeClientIDSecret(ctx, clientID, clientSecret)
|
||||
if err != nil {
|
||||
return err //TODO: wrap?
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
//AuthorizeCodeClientByCodeChallenge authorizes a client by validating the code_verifier against the previously sent
|
||||
//code_challenge of the auth request (PKCE)
|
||||
func AuthorizeCodeChallenge(tokenReq *oidc.AccessTokenRequest, challenge *oidc.CodeChallenge) error {
|
||||
if tokenReq.CodeVerifier == "" {
|
||||
return ErrInvalidRequest("code_challenge required")
|
||||
}
|
||||
if !oidc.VerifyCodeChallenge(challenge, tokenReq.CodeVerifier) {
|
||||
return ErrInvalidRequest("code_challenge invalid")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
//AuthorizePrivateJWTKey authorizes a client by validating the client_assertion's signature with a previously
|
||||
//registered public key (JWT Profile)
|
||||
func AuthorizePrivateJWTKey(ctx context.Context, clientAssertion string, exchanger JWTAuthorizationGrantExchanger) (Client, error) {
|
||||
jwtReq, err := VerifyJWTAssertion(ctx, clientAssertion, exchanger.JWTProfileVerifier())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client, err := exchanger.Storage().GetClientByClientID(ctx, jwtReq.Issuer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if client.AuthMethod() != oidc.AuthMethodPrivateKeyJWT {
|
||||
return nil, ErrInvalidRequest("invalid_client")
|
||||
}
|
||||
return client, nil
|
||||
}
|
|
@ -1,242 +0,0 @@
|
|||
package op
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/caos/oidc/pkg/oidc"
|
||||
"github.com/caos/oidc/pkg/utils"
|
||||
)
|
||||
|
||||
type Exchanger interface {
|
||||
Issuer() string
|
||||
Storage() Storage
|
||||
Decoder() utils.Decoder
|
||||
Signer() Signer
|
||||
Crypto() Crypto
|
||||
AuthMethodPostSupported() bool
|
||||
AuthMethodPrivateKeyJWTSupported() bool
|
||||
GrantTypeTokenExchangeSupported() bool
|
||||
GrantTypeJWTAuthorizationSupported() bool
|
||||
}
|
||||
|
||||
type JWTAuthorizationGrantExchanger interface {
|
||||
Exchanger
|
||||
JWTProfileVerifier() JWTProfileVerifier
|
||||
}
|
||||
|
||||
func tokenHandler(exchanger Exchanger) func(w http.ResponseWriter, r *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.FormValue("grant_type") {
|
||||
case string(oidc.GrantTypeCode):
|
||||
CodeExchange(w, r, exchanger)
|
||||
return
|
||||
case string(oidc.GrantTypeBearer):
|
||||
if ex, ok := exchanger.(JWTAuthorizationGrantExchanger); ok && exchanger.GrantTypeJWTAuthorizationSupported() {
|
||||
JWTProfile(w, r, ex)
|
||||
return
|
||||
}
|
||||
case string(oidc.GrantTypeTokenExchange):
|
||||
if exchanger.GrantTypeTokenExchangeSupported() {
|
||||
TokenExchange(w, r, exchanger)
|
||||
return
|
||||
}
|
||||
case "":
|
||||
RequestError(w, r, ErrInvalidRequest("grant_type missing"))
|
||||
return
|
||||
}
|
||||
RequestError(w, r, ErrInvalidRequest("grant_type not supported"))
|
||||
}
|
||||
}
|
||||
|
||||
func CodeExchange(w http.ResponseWriter, r *http.Request, exchanger Exchanger) {
|
||||
tokenReq, err := ParseAccessTokenRequest(r, exchanger.Decoder())
|
||||
if err != nil {
|
||||
RequestError(w, r, err)
|
||||
}
|
||||
if tokenReq.Code == "" {
|
||||
RequestError(w, r, ErrInvalidRequest("code missing"))
|
||||
return
|
||||
}
|
||||
authReq, client, err := ValidateAccessTokenRequest(r.Context(), tokenReq, exchanger)
|
||||
if err != nil {
|
||||
RequestError(w, r, err)
|
||||
return
|
||||
}
|
||||
resp, err := CreateTokenResponse(r.Context(), authReq, client, exchanger, true, tokenReq.Code)
|
||||
if err != nil {
|
||||
RequestError(w, r, err)
|
||||
return
|
||||
}
|
||||
utils.MarshalJSON(w, resp)
|
||||
}
|
||||
|
||||
func ParseAccessTokenRequest(r *http.Request, decoder utils.Decoder) (*oidc.AccessTokenRequest, error) {
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
return nil, ErrInvalidRequest("error parsing form")
|
||||
}
|
||||
tokenReq := new(oidc.AccessTokenRequest)
|
||||
err = decoder.Decode(tokenReq, r.Form)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidRequest("error decoding form")
|
||||
}
|
||||
clientID, clientSecret, ok := r.BasicAuth()
|
||||
if ok {
|
||||
tokenReq.ClientID, err = url.QueryUnescape(clientID)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidRequest("invalid basic auth header")
|
||||
}
|
||||
tokenReq.ClientSecret, err = url.QueryUnescape(clientSecret)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidRequest("invalid basic auth header")
|
||||
}
|
||||
}
|
||||
return tokenReq, nil
|
||||
}
|
||||
|
||||
func ValidateAccessTokenRequest(ctx context.Context, tokenReq *oidc.AccessTokenRequest, exchanger Exchanger) (AuthRequest, Client, error) {
|
||||
authReq, client, err := AuthorizeClient(ctx, tokenReq, exchanger)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if client.GetID() != authReq.GetClientID() {
|
||||
return nil, nil, ErrInvalidRequest("invalid auth code")
|
||||
}
|
||||
if tokenReq.RedirectURI != authReq.GetRedirectURI() {
|
||||
return nil, nil, ErrInvalidRequest("redirect_uri does no correspond")
|
||||
}
|
||||
return authReq, client, nil
|
||||
}
|
||||
|
||||
func AuthorizeClient(ctx context.Context, tokenReq *oidc.AccessTokenRequest, exchanger Exchanger) (AuthRequest, Client, error) {
|
||||
if tokenReq.ClientAssertionType == oidc.ClientAssertionTypeJWTAssertion {
|
||||
jwtExchanger, ok := exchanger.(JWTAuthorizationGrantExchanger)
|
||||
if !ok || !exchanger.AuthMethodPrivateKeyJWTSupported() {
|
||||
return nil, nil, errors.New("auth_method private_key_jwt not supported")
|
||||
}
|
||||
return AuthorizePrivateJWTKey(ctx, tokenReq, jwtExchanger)
|
||||
}
|
||||
client, err := exchanger.Storage().GetClientByClientID(ctx, tokenReq.ClientID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if client.AuthMethod() == oidc.AuthMethodPrivateKeyJWT {
|
||||
return nil, nil, errors.New("invalid_grant")
|
||||
}
|
||||
if client.AuthMethod() == oidc.AuthMethodNone {
|
||||
authReq, err := AuthorizeCodeChallenge(ctx, tokenReq, exchanger)
|
||||
return authReq, client, err
|
||||
}
|
||||
if client.AuthMethod() == oidc.AuthMethodPost && !exchanger.AuthMethodPostSupported() {
|
||||
return nil, nil, errors.New("auth_method post not supported")
|
||||
}
|
||||
authReq, err := AuthorizeClientIDSecret(ctx, tokenReq.ClientID, tokenReq.ClientSecret, tokenReq.Code, exchanger.Storage())
|
||||
return authReq, client, err
|
||||
}
|
||||
|
||||
func AuthorizePrivateJWTKey(ctx context.Context, tokenReq *oidc.AccessTokenRequest, exchanger JWTAuthorizationGrantExchanger) (AuthRequest, Client, error) {
|
||||
jwtReq, err := VerifyJWTAssertion(ctx, tokenReq.ClientAssertion, exchanger.JWTProfileVerifier())
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
authReq, err := exchanger.Storage().AuthRequestByCode(ctx, tokenReq.Code)
|
||||
if err != nil {
|
||||
return nil, nil, ErrInvalidRequest("invalid code")
|
||||
}
|
||||
client, err := exchanger.Storage().GetClientByClientID(ctx, jwtReq.Issuer)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if client.AuthMethod() != oidc.AuthMethodPrivateKeyJWT {
|
||||
return nil, nil, ErrInvalidRequest("invalid_client")
|
||||
}
|
||||
return authReq, client, nil
|
||||
}
|
||||
|
||||
func AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret, code string, storage Storage) (AuthRequest, error) {
|
||||
err := storage.AuthorizeClientIDSecret(ctx, clientID, clientSecret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
authReq, err := storage.AuthRequestByCode(ctx, code)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidRequest("invalid code")
|
||||
}
|
||||
return authReq, nil
|
||||
}
|
||||
|
||||
func AuthorizeCodeChallenge(ctx context.Context, tokenReq *oidc.AccessTokenRequest, exchanger Exchanger) (AuthRequest, error) {
|
||||
if tokenReq.CodeVerifier == "" {
|
||||
return nil, ErrInvalidRequest("code_challenge required")
|
||||
}
|
||||
authReq, err := exchanger.Storage().AuthRequestByCode(ctx, tokenReq.Code)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidRequest("invalid code")
|
||||
}
|
||||
if !oidc.VerifyCodeChallenge(authReq.GetCodeChallenge(), tokenReq.CodeVerifier) {
|
||||
return nil, ErrInvalidRequest("code_challenge invalid")
|
||||
}
|
||||
return authReq, nil
|
||||
}
|
||||
|
||||
func JWTProfile(w http.ResponseWriter, r *http.Request, exchanger JWTAuthorizationGrantExchanger) {
|
||||
profileRequest, err := ParseJWTProfileRequest(r, exchanger.Decoder())
|
||||
if err != nil {
|
||||
RequestError(w, r, err)
|
||||
}
|
||||
|
||||
tokenRequest, err := VerifyJWTAssertion(r.Context(), profileRequest.Assertion, exchanger.JWTProfileVerifier())
|
||||
if err != nil {
|
||||
RequestError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
tokenRequest.Scopes, err = exchanger.Storage().ValidateJWTProfileScopes(r.Context(), tokenRequest.Issuer, profileRequest.Scope)
|
||||
if err != nil {
|
||||
RequestError(w, r, err)
|
||||
return
|
||||
}
|
||||
resp, err := CreateJWTTokenResponse(r.Context(), tokenRequest, exchanger)
|
||||
if err != nil {
|
||||
RequestError(w, r, err)
|
||||
return
|
||||
}
|
||||
utils.MarshalJSON(w, resp)
|
||||
}
|
||||
|
||||
func ParseJWTProfileRequest(r *http.Request, decoder utils.Decoder) (*oidc.JWTProfileGrantRequest, error) {
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
return nil, ErrInvalidRequest("error parsing form")
|
||||
}
|
||||
tokenReq := new(oidc.JWTProfileGrantRequest)
|
||||
err = decoder.Decode(tokenReq, r.Form)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidRequest("error decoding form")
|
||||
}
|
||||
return tokenReq, nil
|
||||
}
|
||||
|
||||
func TokenExchange(w http.ResponseWriter, r *http.Request, exchanger Exchanger) {
|
||||
tokenRequest, err := ParseTokenExchangeRequest(w, r)
|
||||
if err != nil {
|
||||
RequestError(w, r, err)
|
||||
return
|
||||
}
|
||||
err = ValidateTokenExchangeRequest(tokenRequest, exchanger.Storage())
|
||||
if err != nil {
|
||||
RequestError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func ParseTokenExchangeRequest(w http.ResponseWriter, r *http.Request) (oidc.TokenRequest, error) {
|
||||
return nil, errors.New("Unimplemented") //TODO: impl
|
||||
}
|
||||
|
||||
func ValidateTokenExchangeRequest(tokenReq oidc.TokenRequest, storage Storage) error {
|
||||
return errors.New("Unimplemented") //TODO: impl
|
||||
}
|
|
@ -23,6 +23,7 @@ type jwtProfileVerifier struct {
|
|||
offset time.Duration
|
||||
}
|
||||
|
||||
//NewJWTProfileVerifier creates a oidc.Verifier for JWT Profile assertions (authorization grant and client authentication)
|
||||
func NewJWTProfileVerifier(storage Storage, issuer string, maxAgeIAT, offset time.Duration) JWTProfileVerifier {
|
||||
return &jwtProfileVerifier{
|
||||
storage: storage,
|
||||
|
@ -48,6 +49,9 @@ func (v *jwtProfileVerifier) Offset() time.Duration {
|
|||
return v.offset
|
||||
}
|
||||
|
||||
//VerifyJWTAssertion verifies the assertion string from JWT Profile (authorization grant and client authentication)
|
||||
//
|
||||
//checks audience, exp, iat, signature and that issuer and sub are the same
|
||||
func VerifyJWTAssertion(ctx context.Context, assertion string, v JWTProfileVerifier) (*oidc.JWTTokenRequest, error) {
|
||||
request := new(oidc.JWTTokenRequest)
|
||||
payload, err := oidc.ParseToken(assertion, request)
|
||||
|
@ -85,6 +89,7 @@ type jwtProfileKeySet struct {
|
|||
userID string
|
||||
}
|
||||
|
||||
//VerifySignature implements oidc.KeySet by getting the public key from Storage implementation
|
||||
func (k *jwtProfileKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) (payload []byte, err error) {
|
||||
keyID := ""
|
||||
for _, sig := range jws.Signatures {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue