change RP interfaces

This commit is contained in:
Livio Amstutz 2020-09-14 07:52:16 +02:00
parent ed6cbe4fe2
commit 45230569d3
10 changed files with 529 additions and 259 deletions

View file

@ -3,20 +3,22 @@ package cli
import (
"context"
"fmt"
"github.com/caos/oidc/pkg/oidc"
"github.com/caos/oidc/pkg/rp"
"github.com/caos/oidc/pkg/utils"
"github.com/google/uuid"
"github.com/sirupsen/logrus"
"log"
"net/http"
"strings"
"time"
"github.com/google/uuid"
"github.com/sirupsen/logrus"
"github.com/caos/oidc/pkg/oidc"
"github.com/caos/oidc/pkg/rp"
"github.com/caos/oidc/pkg/utils"
)
func CodeFlow(rpc *rp.Config, key []byte, callbackPath string, port string) *oidc.Tokens {
func CodeFlow(rpc *rp.Configuration, key []byte, callbackPath string, port string) *oidc.Tokens {
cookieHandler := utils.NewCookieHandler(key, key, utils.WithUnsecure())
provider, err := rp.NewDefaultRP(rpc, rp.WithCookieHandler(cookieHandler)) //rp.WithPKCE(cookieHandler)) //,
provider, err := rp.NewRelayingParty(rpc, rp.WithCookieHandler(cookieHandler))
if err != nil {
logrus.Fatalf("error creating provider %s", err.Error())
}
@ -24,9 +26,9 @@ func CodeFlow(rpc *rp.Config, key []byte, callbackPath string, port string) *oid
return codeFlow(provider, callbackPath, port)
}
func TokenForClient(rpc *rp.Config, key []byte, token *oidc.Tokens) *http.Client {
func TokenForClient(rpc *rp.Configuration, key []byte, token *oidc.Tokens) *http.Client {
cookieHandler := utils.NewCookieHandler(key, key, utils.WithUnsecure())
provider, err := rp.NewDefaultRP(rpc, rp.WithCookieHandler(cookieHandler)) //rp.WithPKCE(cookieHandler)) //,
provider, err := rp.NewRelayingParty(rpc, rp.WithCookieHandler(cookieHandler))
if err != nil {
logrus.Fatalf("error creating provider %s", err.Error())
}
@ -34,9 +36,9 @@ func TokenForClient(rpc *rp.Config, key []byte, token *oidc.Tokens) *http.Client
return provider.Client(context.Background(), token.Token)
}
func CodeFlowForClient(rpc *rp.Config, key []byte, callbackPath string, port string) *http.Client {
func CodeFlowForClient(rpc *rp.Configuration, key []byte, callbackPath string, port string) *http.Client {
cookieHandler := utils.NewCookieHandler(key, key, utils.WithUnsecure())
provider, err := rp.NewDefaultRP(rpc, rp.WithCookieHandler(cookieHandler)) //rp.WithPKCE(cookieHandler)) //,
provider, err := rp.NewRelayingParty(rpc, rp.WithCookieHandler(cookieHandler))
if err != nil {
logrus.Fatalf("error creating provider %s", err.Error())
}
@ -45,7 +47,7 @@ func CodeFlowForClient(rpc *rp.Config, key []byte, callbackPath string, port str
return provider.Client(context.Background(), token.Token)
}
func codeFlow(provider rp.DelegationTokenExchangeRP, callbackPath string, port string) *oidc.Tokens {
func codeFlow(provider rp.RelayingParty, callbackPath string, port string) *oidc.Tokens {
loginPath := "/login"
portStr := port
if !strings.HasPrefix(port, ":") {
@ -55,12 +57,12 @@ func codeFlow(provider rp.DelegationTokenExchangeRP, callbackPath string, port s
getToken, setToken := getAndSetTokens()
state := uuid.New().String()
http.Handle(loginPath, provider.AuthURLHandler(state))
http.Handle(loginPath, rp.AuthURLHandler(state, provider))
marshal := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string) {
setToken(w, tokens)
}
http.Handle(callbackPath, provider.CodeExchangeHandler(marshal))
http.Handle(callbackPath, rp.CodeExchangeHandler(marshal, provider))
// start http-server
stopHttpServer := startHttpServer(portStr)

View file

@ -72,6 +72,17 @@ type verifierConfig struct {
//ACRVerifier specifies the function to be used by the `DefaultVerifier` for validating the acr claim
type ACRVerifier func(string) error
//DefaultACRVerifier implements `ACRVerifier` returning an error
//if non of the provided values matches the acr claim
func DefaultACRVerifier(possibleValues []string) ACRVerifier {
return func(acr string) error {
if !utils.Contains(possibleValues, acr) {
return fmt.Errorf("expected one of: %v, got: %q", possibleValues, acr)
}
return nil
}
}
func DecryptToken(tokenString string) (string, error) {
return tokenString, nil //TODO: impl
}

View file

@ -7,6 +7,7 @@ import (
"github.com/gorilla/mux"
"github.com/caos/oidc/pkg/oidc"
"github.com/caos/oidc/pkg/utils"
)
const (
@ -16,8 +17,12 @@ const (
type OpenIDProvider interface {
Configuration
Authorizer
SessionEnder
Storage() Storage
Decoder() utils.Decoder
Encoder() utils.Encoder
IDTokenVerifier() IDTokenHintVerifier
Crypto() Crypto
DefaultLogoutRedirectURI() string
Signer() Signer
Probes() []ProbesFn
HttpHandler() http.Handler

View file

@ -2,12 +2,9 @@ package rp
import (
"context"
"encoding/base64"
"net/http"
"strings"
"github.com/caos/oidc/pkg/oidc/grants"
"golang.org/x/oauth2"
"github.com/caos/oidc/pkg/oidc"
@ -21,12 +18,7 @@ const (
pkceCode = "pkce"
)
var (
DefaultErrorHandler = func(w http.ResponseWriter, r *http.Request, errorType string, errorDesc string, state string) {
http.Error(w, errorType+": "+errorDesc, http.StatusInternalServerError)
}
)
//deprecated: use NewRelayingParty instead
//DefaultRP implements the `DelegationTokenExchangeRP` interface extending the `RelayingParty` interface
type DefaultRP struct {
endpoints Endpoints
@ -45,6 +37,36 @@ type DefaultRP struct {
onlyOAuth2 bool
}
func (p *DefaultRP) ErrorHandler() func(http.ResponseWriter, *http.Request, string, string, string) {
return p.errorHandler
}
func (p *DefaultRP) OAuthConfig() *oauth2.Config {
return &p.oauthConfig
}
func (p *DefaultRP) IsPKCE() bool {
return p.pkce
}
func (p *DefaultRP) CookieHandler() *utils.CookieHandler {
return p.cookieHandler
}
func (p *DefaultRP) HttpClient() *http.Client {
return p.httpClient
}
func (p *DefaultRP) IsOAuth2Only() bool {
return p.onlyOAuth2
}
func (p *DefaultRP) IDTokenVerifier() IDTokenVerifier {
return p.idTokenVerifier
}
//deprecated: use NewRelayingParty instead
//
//NewDefaultRP creates `DefaultRP` with the given
//Config and possible configOptions
//it will run discovery on the provided issuer
@ -89,6 +111,7 @@ func NewDefaultRP(rpConfig *Config, rpOpts ...DefaultRPOpts) (DelegationTokenExc
//DefaultRPOpts is the type for providing dynamic options to the DefaultRP
type DefaultRPOpts func(p *DefaultRP)
/*
//WithCookieHandler set a `CookieHandler` for securing the various redirects
func WithCookieHandler(cookieHandler *utils.CookieHandler) DefaultRPOpts {
return func(p *DefaultRP) {
@ -118,108 +141,34 @@ func WithVerifierOpts(opts ...ConfFunc) DefaultRPOpts {
p.verifierOpts = opts
}
}
*/
//AuthURL is the `RelayingParty` interface implementation
//wrapping the oauth2 `AuthCodeURL`
//returning the url of the auth request
func (p *DefaultRP) AuthURL(state string, opts ...AuthURLOpt) string {
authOpts := make([]oauth2.AuthCodeOption, 0)
for _, opt := range opts {
authOpts = append(authOpts, opt()...)
}
return p.oauthConfig.AuthCodeURL(state, authOpts...)
return AuthURL(state, p, opts...)
}
//AuthURL is the `RelayingParty` interface implementation
//extending the `AuthURL` method with a http redirect handler
func (p *DefaultRP) AuthURLHandler(state string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
opts := make([]AuthURLOpt, 0)
if err := p.trySetStateCookie(w, state); err != nil {
http.Error(w, "failed to create state cookie: "+err.Error(), http.StatusUnauthorized)
return
}
if p.pkce {
codeChallenge, err := p.generateAndStoreCodeChallenge(w)
if err != nil {
http.Error(w, "failed to create code challenge: "+err.Error(), http.StatusUnauthorized)
return
}
opts = append(opts, WithCodeChallenge(codeChallenge))
}
http.Redirect(w, r, p.AuthURL(state, opts...), http.StatusFound)
}
}
func (p *DefaultRP) generateAndStoreCodeChallenge(w http.ResponseWriter) (string, error) {
var codeVerifier string
codeVerifier = "s"
if err := p.cookieHandler.SetCookie(w, pkceCode, codeVerifier); err != nil {
return "", err
}
return oidc.NewSHACodeChallenge(codeVerifier), nil
return AuthURLHandler(state, p)
}
//deprecated: Use CodeExchange func and provide a RelayingParty
//
//AuthURL is the `RelayingParty` interface implementation
//handling the oauth2 code exchange, extracting and validating the id_token
//returning it paresed together with the oauth2 tokens (access, refresh)
//returning it parsed together with the oauth2 tokens (access, refresh)
func (p *DefaultRP) CodeExchange(ctx context.Context, code string, opts ...CodeExchangeOpt) (tokens *oidc.Tokens, err error) {
ctx = context.WithValue(ctx, oauth2.HTTPClient, p.httpClient)
codeOpts := make([]oauth2.AuthCodeOption, 0)
for _, opt := range opts {
codeOpts = append(codeOpts, opt()...)
}
token, err := p.oauthConfig.Exchange(ctx, code, codeOpts...)
if err != nil {
return nil, err //TODO: our error
}
idTokenString, ok := token.Extra(idTokenKey).(string)
if !ok {
//TODO: implement
}
idToken := new(oidc.IDTokenClaims)
if !p.onlyOAuth2 {
idToken, err = VerifyTokens(ctx, token.AccessToken, idTokenString, p.idTokenVerifier)
if err != nil {
return nil, err //TODO: err
}
}
return &oidc.Tokens{Token: token, IDTokenClaims: idToken, IDToken: idTokenString}, nil
return CodeExchange(ctx, code, p, opts...)
}
//AuthURL is the `RelayingParty` interface implementation
//extending the `CodeExchange` method with callback function
func (p *DefaultRP) CodeExchangeHandler(callback func(http.ResponseWriter, *http.Request, *oidc.Tokens, string)) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
state, err := p.tryReadStateCookie(w, r)
if err != nil {
http.Error(w, "failed to get state: "+err.Error(), http.StatusUnauthorized)
return
}
params := r.URL.Query()
if params.Get("error") != "" {
p.errorHandler(w, r, params.Get("error"), params.Get("error_description"), state)
return
}
codeOpts := make([]CodeExchangeOpt, 0)
if p.pkce {
codeVerifier, err := p.cookieHandler.CheckCookie(r, pkceCode)
if err != nil {
http.Error(w, "failed to get code verifier: "+err.Error(), http.StatusUnauthorized)
return
}
codeOpts = append(codeOpts, WithCodeVerifier(codeVerifier))
}
tokens, err := p.CodeExchange(r.Context(), params.Get("code"), codeOpts...)
if err != nil {
http.Error(w, "failed to exchange token: "+err.Error(), http.StatusUnauthorized)
return
}
callback(w, r, tokens, state)
}
return CodeExchangeHandler(callback, p)
}
// func (p *DefaultRP) Introspect(ctx context.Context, accessToken string) (oidc.TokenIntrospectResponse, error) {
@ -237,19 +186,19 @@ func (p *DefaultRP) Userinfo() {}
//ClientCredentials is the `RelayingParty` interface implementation
//handling the oauth2 client credentials grant
func (p *DefaultRP) ClientCredentials(ctx context.Context, scopes ...string) (newToken *oauth2.Token, err error) {
return p.callTokenEndpoint(grants.ClientCredentialsGrantBasic(scopes...))
return ClientCredentials(ctx, p, scopes...)
}
//TokenExchange is the `TokenExchangeRP` interface implementation
//handling the oauth2 token exchange (draft)
func (p *DefaultRP) TokenExchange(ctx context.Context, request *grants_tx.TokenExchangeRequest) (newToken *oauth2.Token, err error) {
return p.callTokenEndpoint(request)
return TokenExchange(ctx, request, p)
}
//DelegationTokenExchange is the `TokenExchangeRP` interface implementation
//handling the oauth2 token exchange for a delegation token (draft)
func (p *DefaultRP) DelegationTokenExchange(ctx context.Context, subjectToken string, reqOpts ...grants_tx.TokenExchangeOption) (newToken *oauth2.Token, err error) {
return p.TokenExchange(ctx, DelegationTokenRequest(subjectToken, reqOpts...))
return TokenExchange(ctx, DelegationTokenRequest(subjectToken, reqOpts...), p)
}
func (p *DefaultRP) discover() error {
@ -278,41 +227,6 @@ func (p *DefaultRP) getOAuthConfig(endpoint oauth2.Endpoint) oauth2.Config {
}
}
func (p *DefaultRP) callTokenEndpoint(request interface{}) (newToken *oauth2.Token, err error) {
req, err := utils.FormRequest(p.endpoints.TokenURL, request)
if err != nil {
return nil, err
}
auth := base64.StdEncoding.EncodeToString([]byte(p.config.ClientID + ":" + p.config.ClientSecret))
req.Header.Set("Authorization", "Basic "+auth)
token := new(oauth2.Token)
if err := utils.HttpRequest(p.httpClient, req, token); err != nil {
return nil, err
}
return token, nil
}
func (p *DefaultRP) trySetStateCookie(w http.ResponseWriter, state string) error {
if p.cookieHandler != nil {
if err := p.cookieHandler.SetCookie(w, stateParam, state); err != nil {
return err
}
}
return nil
}
func (p *DefaultRP) tryReadStateCookie(w http.ResponseWriter, r *http.Request) (state string, err error) {
if p.cookieHandler == nil {
return r.FormValue(stateParam), nil
}
state, err = p.cookieHandler.CheckQueryCookie(r, stateParam)
if err != nil {
return "", err
}
p.cookieHandler.DeleteCookie(w, stateParam)
return state, nil
}
func (p *DefaultRP) Client(ctx context.Context, token *oauth2.Token) *http.Client {
return p.oauthConfig.Client(ctx, token)
}

View file

@ -2,13 +2,12 @@ package rp
import (
"context"
"fmt"
"time"
"github.com/caos/oidc/pkg/oidc"
"github.com/caos/oidc/pkg/utils"
)
//deprecated: use IDTokenVerifier or oidc.Verifier interfaces
//DefaultVerifier implements the `Verifier` interface
type DefaultVerifier struct {
config *verifierConfig
@ -18,6 +17,7 @@ type DefaultVerifier struct {
//ConfFunc is the type for providing dynamic options to the DefaultVerifier
type ConfFunc func(*verifierConfig)
//deprecated: use NewIDTokenVerifier
//NewDefaultVerifier creates `DefaultVerifier` with the given
//issuer, clientID, keyset and possible configOptions
func NewDefaultVerifier(issuer, clientID string, keySet oidc.KeySet, confOpts ...ConfFunc) Verifier {
@ -123,17 +123,14 @@ type iatConfig struct {
maxAge time.Duration
}
//deprecated: use oidc.DefaultACRVerifier directly
//DefaultACRVerifier implements `ACRVerifier` returning an error
//if non of the provided values matches the acr claim
func DefaultACRVerifier(possibleValues []string) oidc.ACRVerifier {
return func(acr string) error {
if !utils.Contains(possibleValues, acr) {
return fmt.Errorf("expected one of: %v, got: %q", possibleValues, acr)
}
return nil
}
return oidc.DefaultACRVerifier(possibleValues)
}
//deprecated: use VerifyTokens(ctx context.Context, accessToken, idTokenString string, v IDTokenVerifier) (*oidc.IDTokenClaims, error) instead
//Verify implements the `Verify` method of the `Verifier` interface
//according to https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
//and https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowTokenValidation
@ -142,6 +139,7 @@ func (v *DefaultVerifier) Verify(ctx context.Context, accessToken, idTokenString
return VerifyTokens(ctx, accessToken, idTokenString, v)
}
//deprecated: use VerifyIDToken(ctx context.Context, token string, v IDTokenVerifier) (*oidc.IDTokenClaims, error) instead
//Verify implements the `VerifyIDToken` method of the `Verifier` interface
//according to https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
func (v *DefaultVerifier) VerifyIDToken(ctx context.Context, idTokenString string) (*oidc.IDTokenClaims, error) {

View file

@ -3,17 +3,30 @@ package rp
import (
"context"
"net/http"
"strings"
"github.com/caos/oidc/pkg/oidc"
"github.com/caos/oidc/pkg/oidc/grants"
"github.com/caos/oidc/pkg/utils"
"golang.org/x/oauth2"
)
//RelayingParty declares the minimal interface for oidc clients
type RelayingParty interface {
//OAuthConfig returns the oauth2 Config
OAuthConfig() *oauth2.Config
//IsPKCE returns if authorization is done using `Authorization Code Flow with Proof Key for Code Exchange (PKCE)`
IsPKCE() bool
//CookieHandler returns a http cookie handler used for various state transfer cookies
CookieHandler() *utils.CookieHandler
//Client return a standard http client where the token can be used
Client(ctx context.Context, token *oauth2.Token) *http.Client
/*
//AuthURL returns the authorization endpoint with a given state
AuthURL(state string, opts ...AuthURLOpt) string
@ -40,28 +53,314 @@ type RelayingParty interface {
//Userinfo implements the OIDC Userinfo call
//returning the info of the user for the requested scopes of an access token
Userinfo()
*/
HttpClient() *http.Client
IsOAuth2Only() bool
IDTokenVerifier() IDTokenVerifier
ErrorHandler() func(http.ResponseWriter, *http.Request, string, string, string)
}
//PasswortGrantRP extends the `RelayingParty` interface with the oauth2 `Password Grant`
//
//This interface is separated from the standard `RelayingParty` interface as the `password grant`
//is part of the oauth2 and therefore OIDC specification, but should only be used when there's no
//other possibility, so IMHO never ever. Ever.
type PasswortGrantRP interface {
RelayingParty
////PasswortGrantRP extends the `RelayingParty` interface with the oauth2 `Password Grant`
////
////This interface is separated from the standard `RelayingParty` interface as the `password grant`
////is part of the oauth2 and therefore OIDC specification, but should only be used when there's no
////other possibility, so IMHO never ever. Ever.
//type PasswortGrantRP interface {
// RelayingParty
//
// //PasswordGrant implements the oauth2 `Password Grant`,
// //requesting an access token with the users `username` and `password`
// PasswordGrant(context.Context, string, string) (*oauth2.Token, error)
//}
//PasswordGrant implements the oauth2 `Password Grant`,
//requesting an access token with the users `username` and `password`
PasswordGrant(context.Context, string, string) (*oauth2.Token, error)
var (
DefaultErrorHandler = func(w http.ResponseWriter, r *http.Request, errorType string, errorDesc string, state string) {
http.Error(w, errorType+": "+errorDesc, http.StatusInternalServerError)
}
)
type relayingParty struct {
endpoints Endpoints
oauthConfig *oauth2.Config
config *Configuration
pkce bool
httpClient *http.Client
cookieHandler *utils.CookieHandler
errorHandler func(http.ResponseWriter, *http.Request, string, string, string)
idTokenVerifier IDTokenVerifier
verifierOpts []ConfFunc
oauth2Only bool
}
type Config struct {
ClientID string
ClientSecret string
CallbackURL string
func (rp *relayingParty) OAuthConfig() *oauth2.Config {
return rp.oauthConfig
}
func (rp *relayingParty) IsPKCE() bool {
return rp.pkce
}
func (rp *relayingParty) CookieHandler() *utils.CookieHandler {
return rp.cookieHandler
}
func (rp *relayingParty) HttpClient() *http.Client {
return rp.httpClient
}
func (rp *relayingParty) IsOAuth2Only() bool {
return rp.oauth2Only
}
func (rp *relayingParty) IDTokenVerifier() IDTokenVerifier {
return rp.idTokenVerifier
}
func (rp *relayingParty) Client(ctx context.Context, token *oauth2.Token) *http.Client {
panic("implement me")
}
func (rp *relayingParty) ErrorHandler() func(http.ResponseWriter, *http.Request, string, string, string) {
return rp.errorHandler
}
//NewRelayingParty creates a DelegationTokenExchangeRP with the given
//Config and possible configOptions
//it will run discovery on the provided issuer if AuthURL and TokenURL are not set
//if no verifier is provided using the options the `DefaultVerifier` is set
func NewRelayingParty(config *Configuration, options ...Option) (RelayingParty, error) {
isOpenID := isOpenID(config.Scopes)
rp := &relayingParty{
config: config,
httpClient: utils.DefaultHTTPClient,
oauth2Only: !isOpenID,
}
for _, optFunc := range options {
optFunc(rp)
}
rp.oauthConfig = config.Config
if config.Endpoint.AuthURL != "" && config.Endpoint.TokenURL != "" {
rp.oauthConfig = config.Config
} else {
endpoints, err := Discover(config.Issuer, rp.httpClient)
if err != nil {
return nil, err
}
rp.oauthConfig.Endpoint = endpoints.Endpoint
}
if rp.errorHandler == nil {
rp.errorHandler = DefaultErrorHandler
}
if isOpenID && rp.idTokenVerifier == nil {
rp.idTokenVerifier = NewIDTokenVerifier(config.Issuer, config.ClientID, NewRemoteKeySet(rp.httpClient, rp.endpoints.JKWsURL))
}
return rp, nil
}
//DefaultRPOpts is the type for providing dynamic options to the DefaultRP
type Option func(*relayingParty)
//WithCookieHandler set a `CookieHandler` for securing the various redirects
func WithCookieHandler(cookieHandler *utils.CookieHandler) Option {
return func(rp *relayingParty) {
rp.cookieHandler = cookieHandler
}
}
//WithPKCE sets the RP to use PKCE (oauth2 code challenge)
//it also sets a `CookieHandler` for securing the various redirects
//and exchanging the code challenge
func WithPKCE(cookieHandler *utils.CookieHandler) Option {
return func(rp *relayingParty) {
rp.pkce = true
rp.cookieHandler = cookieHandler
}
}
//WithHTTPClient provides the ability to set an http client to be used for the relaying party and verifier
func WithHTTPClient(client *http.Client) Option {
return func(rp *relayingParty) {
rp.httpClient = client
}
}
func WithVerifierOpts(opts ...ConfFunc) Option {
return func(rp *relayingParty) {
rp.verifierOpts = opts
}
}
//Discover calls the discovery endpoint of the provided issuer and returns the found endpoints
func Discover(issuer string, httpClient *http.Client) (Endpoints, error) {
wellKnown := strings.TrimSuffix(issuer, "/") + oidc.DiscoveryEndpoint
req, err := http.NewRequest("GET", wellKnown, nil)
if err != nil {
return Endpoints{}, err
}
discoveryConfig := new(oidc.DiscoveryConfiguration)
err = utils.HttpRequest(httpClient, req, &discoveryConfig)
if err != nil {
return Endpoints{}, err
}
return GetEndpoints(discoveryConfig), nil
}
//AuthURL returns the auth request url
//(wrapping the oauth2 `AuthCodeURL`)
func AuthURL(state string, rp RelayingParty, opts ...AuthURLOpt) string {
authOpts := make([]oauth2.AuthCodeOption, 0)
for _, opt := range opts {
authOpts = append(authOpts, opt()...)
}
return rp.OAuthConfig().AuthCodeURL(state, authOpts...)
}
//AuthURLHandler extends the `AuthURL` method with a http redirect handler
//including handling setting cookie for secure `state` transfer
func AuthURLHandler(state string, rp RelayingParty) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
opts := make([]AuthURLOpt, 0)
if err := trySetStateCookie(w, state, rp); err != nil {
http.Error(w, "failed to create state cookie: "+err.Error(), http.StatusUnauthorized)
return
}
if rp.IsPKCE() {
codeChallenge, err := GenerateAndStoreCodeChallenge(w, rp)
if err != nil {
http.Error(w, "failed to create code challenge: "+err.Error(), http.StatusUnauthorized)
return
}
opts = append(opts, WithCodeChallenge(codeChallenge))
}
http.Redirect(w, r, AuthURL(state, rp, opts...), http.StatusFound)
}
}
func GenerateAndStoreCodeChallenge(w http.ResponseWriter, rp RelayingParty) (string, error) {
var codeVerifier string
codeVerifier = "s"
if err := rp.CookieHandler().SetCookie(w, pkceCode, codeVerifier); err != nil {
return "", err
}
return oidc.NewSHACodeChallenge(codeVerifier), nil
}
//AuthURL is the `RelayingParty` interface implementation
//handling the oauth2 code exchange, extracting and validating the id_token
//returning it paresed together with the oauth2 tokens (access, refresh)
func CodeExchange(ctx context.Context, code string, rp RelayingParty, opts ...CodeExchangeOpt) (tokens *oidc.Tokens, err error) {
ctx = context.WithValue(ctx, oauth2.HTTPClient, rp.HttpClient())
codeOpts := make([]oauth2.AuthCodeOption, 0)
for _, opt := range opts {
codeOpts = append(codeOpts, opt()...)
}
token, err := rp.OAuthConfig().Exchange(ctx, code, codeOpts...)
if err != nil {
return nil, err //TODO: our error
}
idTokenString, ok := token.Extra(idTokenKey).(string)
if !ok {
//TODO: implement
}
idToken := new(oidc.IDTokenClaims)
if !rp.IsOAuth2Only() {
idToken, err = VerifyTokens(ctx, token.AccessToken, idTokenString, rp.IDTokenVerifier())
if err != nil {
return nil, err //TODO: err
}
}
return &oidc.Tokens{Token: token, IDTokenClaims: idToken, IDToken: idTokenString}, nil
}
//AuthURL is the `RelayingParty` interface implementation
//extending the `CodeExchange` method with callback function
func CodeExchangeHandler(callback func(http.ResponseWriter, *http.Request, *oidc.Tokens, string), rp RelayingParty) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
state, err := tryReadStateCookie(w, r, rp)
if err != nil {
http.Error(w, "failed to get state: "+err.Error(), http.StatusUnauthorized)
return
}
params := r.URL.Query()
if params.Get("error") != "" {
rp.ErrorHandler()(w, r, params.Get("error"), params.Get("error_description"), state)
return
}
codeOpts := make([]CodeExchangeOpt, 0)
if rp.IsPKCE() {
codeVerifier, err := rp.CookieHandler().CheckCookie(r, pkceCode)
if err != nil {
http.Error(w, "failed to get code verifier: "+err.Error(), http.StatusUnauthorized)
return
}
codeOpts = append(codeOpts, WithCodeVerifier(codeVerifier))
}
tokens, err := CodeExchange(r.Context(), params.Get("code"), rp, codeOpts...)
if err != nil {
http.Error(w, "failed to exchange token: "+err.Error(), http.StatusUnauthorized)
return
}
callback(w, r, tokens, state)
}
}
//ClientCredentials is the `RelayingParty` interface implementation
//handling the oauth2 client credentials grant
func ClientCredentials(ctx context.Context, rp RelayingParty, scopes ...string) (newToken *oauth2.Token, err error) {
return CallTokenEndpoint(grants.ClientCredentialsGrantBasic(scopes...), rp)
}
func CallTokenEndpoint(request interface{}, rp RelayingParty) (newToken *oauth2.Token, err error) {
config := rp.OAuthConfig()
req, err := utils.FormRequest(rp.OAuthConfig().Endpoint.TokenURL, request, config.ClientID, config.ClientSecret, config.Endpoint.AuthStyle != oauth2.AuthStyleInParams)
if err != nil {
return nil, err
}
token := new(oauth2.Token)
if err := utils.HttpRequest(rp.HttpClient(), req, token); err != nil {
return nil, err
}
return token, nil
}
func trySetStateCookie(w http.ResponseWriter, state string, rp RelayingParty) error {
if rp.CookieHandler() != nil {
if err := rp.CookieHandler().SetCookie(w, stateParam, state); err != nil {
return err
}
}
return nil
}
func tryReadStateCookie(w http.ResponseWriter, r *http.Request, rp RelayingParty) (state string, err error) {
if rp.CookieHandler() == nil {
return r.FormValue(stateParam), nil
}
state, err = rp.CookieHandler().CheckQueryCookie(r, stateParam)
if err != nil {
return "", err
}
rp.CookieHandler().DeleteCookie(w, stateParam)
return state, nil
}
type Configuration struct {
Issuer string
Scopes []string
Endpoints oauth2.Endpoint
*oauth2.Config
}
type OptionFunc func(RelayingParty)
@ -106,3 +405,22 @@ func WithCodeVerifier(codeVerifier string) CodeExchangeOpt {
return []oauth2.AuthCodeOption{oauth2.SetAuthURLParam("code_verifier", codeVerifier)}
}
}
func isOpenID(scopes []string) bool {
for _, scope := range scopes {
if scope == oidc.ScopeOpenID {
return true
}
}
return false
}
//deprecated: use Configuration instead
type Config struct {
ClientID string
ClientSecret string
CallbackURL string
Issuer string
Scopes []string
Endpoints oauth2.Endpoint
}

View file

@ -12,7 +12,7 @@ import (
type TokenExchangeRP interface {
RelayingParty
//TokenExchange implement the `Token Echange Grant` exchanging some token for an other
//TokenExchange implement the `Token Exchange Grant` exchanging some token for an other
TokenExchange(context.Context, *tokenexchange.TokenExchangeRequest) (*oauth2.Token, error)
}
@ -25,3 +25,15 @@ type DelegationTokenExchangeRP interface {
//providing an access token in request for a `delegation` token for a given resource / audience
DelegationTokenExchange(context.Context, string, ...tokenexchange.TokenExchangeOption) (*oauth2.Token, error)
}
//TokenExchange is the `TokenExchangeRP` interface implementation
//handling the oauth2 token exchange (draft)
func TokenExchange(ctx context.Context, request *tokenexchange.TokenExchangeRequest, rp RelayingParty) (newToken *oauth2.Token, err error) {
return CallTokenEndpoint(request, rp)
}
//DelegationTokenExchange is the `TokenExchangeRP` interface implementation
//handling the oauth2 token exchange for a delegation token (draft)
func DelegationTokenExchange(ctx context.Context, subjectToken string, rp RelayingParty, reqOpts ...tokenexchange.TokenExchangeOption) (newToken *oauth2.Token, err error) {
return TokenExchange(ctx, DelegationTokenRequest(subjectToken, reqOpts...), rp)
}

View file

@ -9,16 +9,6 @@ import (
"github.com/caos/oidc/pkg/oidc"
)
//deprecated: Use IDTokenVerifier or oidc.Verifier
type Verifier interface {
//Verify checks the access_token and id_token and returns the `id token claims`
Verify(ctx context.Context, accessToken, idTokenString string) (*oidc.IDTokenClaims, error)
//VerifyIDToken checks the id_token only and returns its `id token claims`
VerifyIDToken(ctx context.Context, idTokenString string) (*oidc.IDTokenClaims, error)
}
type IDTokenVerifier interface {
oidc.Verifier
ClientID() string
@ -29,63 +19,6 @@ type IDTokenVerifier interface {
MaxAge() time.Duration
}
type idTokenVerifier struct {
issuer string
maxAgeIAT time.Duration
offset time.Duration
clientID string
supportedSignAlgs []string
keySet oidc.KeySet
acr oidc.ACRVerifier
maxAge time.Duration
nonce func(ctx context.Context) string
}
func (i *idTokenVerifier) Issuer() string {
return i.issuer
}
func (i *idTokenVerifier) MaxAgeIAT() time.Duration {
return i.maxAgeIAT
}
func (i *idTokenVerifier) Offset() time.Duration {
return i.offset
}
func (i *idTokenVerifier) ClientID() string {
return i.clientID
}
func (i *idTokenVerifier) SupportedSignAlgs() []string {
return i.supportedSignAlgs
}
func (i *idTokenVerifier) KeySet() oidc.KeySet {
return i.keySet
}
func (i *idTokenVerifier) Nonce(ctx context.Context) string {
return i.nonce(ctx)
}
func (i *idTokenVerifier) ACR() oidc.ACRVerifier {
return i.acr
}
func (i *idTokenVerifier) MaxAge() time.Duration {
return i.maxAge
}
func NewIDTokenVerifier(issuer, clientID string, keySet oidc.KeySet) IDTokenVerifier {
return &idTokenVerifier{
issuer: issuer,
clientID: clientID,
keySet: keySet,
offset: 5 * time.Second,
}
}
//VerifyTokens implement the Token Response Validation as defined in OIDC specification
//https://openid.net/specs/openid-connect-core-1_0.html#TokenResponseValidation
func VerifyTokens(ctx context.Context, accessToken, idTokenString string, v IDTokenVerifier) (*oidc.IDTokenClaims, error) {
@ -167,3 +100,72 @@ func VerifyAccessToken(accessToken, atHash string, sigAlgorithm jose.SignatureAl
}
return nil
}
//NewIDTokenVerifier returns an implementation of `IDTokenVerifier`
//for `VerifyTokens` and `VerifyIDToken`
func NewIDTokenVerifier(issuer, clientID string, keySet oidc.KeySet) IDTokenVerifier {
return &idTokenVerifier{
issuer: issuer,
clientID: clientID,
keySet: keySet,
offset: 5 * time.Second,
}
}
type idTokenVerifier struct {
issuer string
maxAgeIAT time.Duration
offset time.Duration
clientID string
supportedSignAlgs []string
keySet oidc.KeySet
acr oidc.ACRVerifier
maxAge time.Duration
nonce func(ctx context.Context) string
}
func (i *idTokenVerifier) Issuer() string {
return i.issuer
}
func (i *idTokenVerifier) MaxAgeIAT() time.Duration {
return i.maxAgeIAT
}
func (i *idTokenVerifier) Offset() time.Duration {
return i.offset
}
func (i *idTokenVerifier) ClientID() string {
return i.clientID
}
func (i *idTokenVerifier) SupportedSignAlgs() []string {
return i.supportedSignAlgs
}
func (i *idTokenVerifier) KeySet() oidc.KeySet {
return i.keySet
}
func (i *idTokenVerifier) Nonce(ctx context.Context) string {
return i.nonce(ctx)
}
func (i *idTokenVerifier) ACR() oidc.ACRVerifier {
return i.acr
}
func (i *idTokenVerifier) MaxAge() time.Duration {
return i.maxAge
}
//deprecated: Use IDTokenVerifier (or oidc.Verifier)
type Verifier interface {
//Verify checks the access_token and id_token and returns the `id token claims`
Verify(ctx context.Context, accessToken, idTokenString string) (*oidc.IDTokenClaims, error)
//VerifyIDToken checks the id_token only and returns its `id token claims`
VerifyIDToken(ctx context.Context, idTokenString string) (*oidc.IDTokenClaims, error)
}

View file

@ -24,7 +24,8 @@ func GetHashAlgorithm(sigAlgorithm jose.SignatureAlgorithm) (hash.Hash, error) {
}
func HashString(hash hash.Hash, s string, firstHalf bool) string {
hash.Write([]byte(s)) // hash documents that Write will never return an error
//nolint:errcheck
hash.Write([]byte(s))
size := hash.Size()
if firstHalf {
size = size / 2

View file

@ -25,17 +25,24 @@ type Encoder interface {
Encode(src interface{}, dst map[string][]string) error
}
func FormRequest(endpoint string, request interface{}) (*http.Request, error) {
func FormRequest(endpoint string, request interface{}, clientID, clientSecret string, header bool) (*http.Request, error) {
form := make(map[string][]string)
encoder := schema.NewEncoder()
if err := encoder.Encode(request, form); err != nil {
return nil, err
}
if !header {
form["client_id"] = []string{clientID}
form["client_secret"] = []string{clientSecret}
}
body := strings.NewReader(url.Values(form).Encode())
req, err := http.NewRequest("POST", endpoint, body)
if err != nil {
return nil, err
}
if header {
req.SetBasicAuth(clientID, clientSecret)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
return req, nil
}