change RP interfaces
This commit is contained in:
parent
ed6cbe4fe2
commit
45230569d3
10 changed files with 529 additions and 259 deletions
|
@ -3,20 +3,22 @@ package cli
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"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"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"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())
|
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 {
|
if err != nil {
|
||||||
logrus.Fatalf("error creating provider %s", err.Error())
|
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)
|
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())
|
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 {
|
if err != nil {
|
||||||
logrus.Fatalf("error creating provider %s", err.Error())
|
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)
|
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())
|
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 {
|
if err != nil {
|
||||||
logrus.Fatalf("error creating provider %s", err.Error())
|
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)
|
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"
|
loginPath := "/login"
|
||||||
portStr := port
|
portStr := port
|
||||||
if !strings.HasPrefix(port, ":") {
|
if !strings.HasPrefix(port, ":") {
|
||||||
|
@ -55,12 +57,12 @@ func codeFlow(provider rp.DelegationTokenExchangeRP, callbackPath string, port s
|
||||||
getToken, setToken := getAndSetTokens()
|
getToken, setToken := getAndSetTokens()
|
||||||
|
|
||||||
state := uuid.New().String()
|
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) {
|
marshal := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string) {
|
||||||
setToken(w, tokens)
|
setToken(w, tokens)
|
||||||
}
|
}
|
||||||
http.Handle(callbackPath, provider.CodeExchangeHandler(marshal))
|
http.Handle(callbackPath, rp.CodeExchangeHandler(marshal, provider))
|
||||||
|
|
||||||
// start http-server
|
// start http-server
|
||||||
stopHttpServer := startHttpServer(portStr)
|
stopHttpServer := startHttpServer(portStr)
|
||||||
|
|
|
@ -72,6 +72,17 @@ type verifierConfig struct {
|
||||||
//ACRVerifier specifies the function to be used by the `DefaultVerifier` for validating the acr claim
|
//ACRVerifier specifies the function to be used by the `DefaultVerifier` for validating the acr claim
|
||||||
type ACRVerifier func(string) error
|
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) {
|
func DecryptToken(tokenString string) (string, error) {
|
||||||
return tokenString, nil //TODO: impl
|
return tokenString, nil //TODO: impl
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
"github.com/caos/oidc/pkg/oidc"
|
"github.com/caos/oidc/pkg/oidc"
|
||||||
|
"github.com/caos/oidc/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -16,8 +17,12 @@ const (
|
||||||
|
|
||||||
type OpenIDProvider interface {
|
type OpenIDProvider interface {
|
||||||
Configuration
|
Configuration
|
||||||
Authorizer
|
Storage() Storage
|
||||||
SessionEnder
|
Decoder() utils.Decoder
|
||||||
|
Encoder() utils.Encoder
|
||||||
|
IDTokenVerifier() IDTokenHintVerifier
|
||||||
|
Crypto() Crypto
|
||||||
|
DefaultLogoutRedirectURI() string
|
||||||
Signer() Signer
|
Signer() Signer
|
||||||
Probes() []ProbesFn
|
Probes() []ProbesFn
|
||||||
HttpHandler() http.Handler
|
HttpHandler() http.Handler
|
||||||
|
|
|
@ -2,12 +2,9 @@ package rp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/caos/oidc/pkg/oidc/grants"
|
|
||||||
|
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
|
|
||||||
"github.com/caos/oidc/pkg/oidc"
|
"github.com/caos/oidc/pkg/oidc"
|
||||||
|
@ -21,12 +18,7 @@ const (
|
||||||
pkceCode = "pkce"
|
pkceCode = "pkce"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
//deprecated: use NewRelayingParty instead
|
||||||
DefaultErrorHandler = func(w http.ResponseWriter, r *http.Request, errorType string, errorDesc string, state string) {
|
|
||||||
http.Error(w, errorType+": "+errorDesc, http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
//DefaultRP implements the `DelegationTokenExchangeRP` interface extending the `RelayingParty` interface
|
//DefaultRP implements the `DelegationTokenExchangeRP` interface extending the `RelayingParty` interface
|
||||||
type DefaultRP struct {
|
type DefaultRP struct {
|
||||||
endpoints Endpoints
|
endpoints Endpoints
|
||||||
|
@ -45,6 +37,36 @@ type DefaultRP struct {
|
||||||
onlyOAuth2 bool
|
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
|
//NewDefaultRP creates `DefaultRP` with the given
|
||||||
//Config and possible configOptions
|
//Config and possible configOptions
|
||||||
//it will run discovery on the provided issuer
|
//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
|
//DefaultRPOpts is the type for providing dynamic options to the DefaultRP
|
||||||
type DefaultRPOpts func(p *DefaultRP)
|
type DefaultRPOpts func(p *DefaultRP)
|
||||||
|
|
||||||
|
/*
|
||||||
//WithCookieHandler set a `CookieHandler` for securing the various redirects
|
//WithCookieHandler set a `CookieHandler` for securing the various redirects
|
||||||
func WithCookieHandler(cookieHandler *utils.CookieHandler) DefaultRPOpts {
|
func WithCookieHandler(cookieHandler *utils.CookieHandler) DefaultRPOpts {
|
||||||
return func(p *DefaultRP) {
|
return func(p *DefaultRP) {
|
||||||
|
@ -118,108 +141,34 @@ func WithVerifierOpts(opts ...ConfFunc) DefaultRPOpts {
|
||||||
p.verifierOpts = opts
|
p.verifierOpts = opts
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
//AuthURL is the `RelayingParty` interface implementation
|
//AuthURL is the `RelayingParty` interface implementation
|
||||||
//wrapping the oauth2 `AuthCodeURL`
|
//wrapping the oauth2 `AuthCodeURL`
|
||||||
//returning the url of the auth request
|
//returning the url of the auth request
|
||||||
func (p *DefaultRP) AuthURL(state string, opts ...AuthURLOpt) string {
|
func (p *DefaultRP) AuthURL(state string, opts ...AuthURLOpt) string {
|
||||||
authOpts := make([]oauth2.AuthCodeOption, 0)
|
return AuthURL(state, p, opts...)
|
||||||
for _, opt := range opts {
|
|
||||||
authOpts = append(authOpts, opt()...)
|
|
||||||
}
|
|
||||||
return p.oauthConfig.AuthCodeURL(state, authOpts...)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//AuthURL is the `RelayingParty` interface implementation
|
//AuthURL is the `RelayingParty` interface implementation
|
||||||
//extending the `AuthURL` method with a http redirect handler
|
//extending the `AuthURL` method with a http redirect handler
|
||||||
func (p *DefaultRP) AuthURLHandler(state string) http.HandlerFunc {
|
func (p *DefaultRP) AuthURLHandler(state string) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return AuthURLHandler(state, p)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//deprecated: Use CodeExchange func and provide a RelayingParty
|
||||||
|
//
|
||||||
//AuthURL is the `RelayingParty` interface implementation
|
//AuthURL is the `RelayingParty` interface implementation
|
||||||
//handling the oauth2 code exchange, extracting and validating the id_token
|
//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) {
|
func (p *DefaultRP) CodeExchange(ctx context.Context, code string, opts ...CodeExchangeOpt) (tokens *oidc.Tokens, err error) {
|
||||||
ctx = context.WithValue(ctx, oauth2.HTTPClient, p.httpClient)
|
return CodeExchange(ctx, code, p, opts...)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//AuthURL is the `RelayingParty` interface implementation
|
//AuthURL is the `RelayingParty` interface implementation
|
||||||
//extending the `CodeExchange` method with callback function
|
//extending the `CodeExchange` method with callback function
|
||||||
func (p *DefaultRP) CodeExchangeHandler(callback func(http.ResponseWriter, *http.Request, *oidc.Tokens, string)) http.HandlerFunc {
|
func (p *DefaultRP) CodeExchangeHandler(callback func(http.ResponseWriter, *http.Request, *oidc.Tokens, string)) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return CodeExchangeHandler(callback, p)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// func (p *DefaultRP) Introspect(ctx context.Context, accessToken string) (oidc.TokenIntrospectResponse, error) {
|
// 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
|
//ClientCredentials is the `RelayingParty` interface implementation
|
||||||
//handling the oauth2 client credentials grant
|
//handling the oauth2 client credentials grant
|
||||||
func (p *DefaultRP) ClientCredentials(ctx context.Context, scopes ...string) (newToken *oauth2.Token, err error) {
|
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
|
//TokenExchange is the `TokenExchangeRP` interface implementation
|
||||||
//handling the oauth2 token exchange (draft)
|
//handling the oauth2 token exchange (draft)
|
||||||
func (p *DefaultRP) TokenExchange(ctx context.Context, request *grants_tx.TokenExchangeRequest) (newToken *oauth2.Token, err error) {
|
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
|
//DelegationTokenExchange is the `TokenExchangeRP` interface implementation
|
||||||
//handling the oauth2 token exchange for a delegation token (draft)
|
//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) {
|
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 {
|
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 {
|
func (p *DefaultRP) Client(ctx context.Context, token *oauth2.Token) *http.Client {
|
||||||
return p.oauthConfig.Client(ctx, token)
|
return p.oauthConfig.Client(ctx, token)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,13 +2,12 @@ package rp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/caos/oidc/pkg/oidc"
|
"github.com/caos/oidc/pkg/oidc"
|
||||||
"github.com/caos/oidc/pkg/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//deprecated: use IDTokenVerifier or oidc.Verifier interfaces
|
||||||
//DefaultVerifier implements the `Verifier` interface
|
//DefaultVerifier implements the `Verifier` interface
|
||||||
type DefaultVerifier struct {
|
type DefaultVerifier struct {
|
||||||
config *verifierConfig
|
config *verifierConfig
|
||||||
|
@ -18,6 +17,7 @@ type DefaultVerifier struct {
|
||||||
//ConfFunc is the type for providing dynamic options to the DefaultVerifier
|
//ConfFunc is the type for providing dynamic options to the DefaultVerifier
|
||||||
type ConfFunc func(*verifierConfig)
|
type ConfFunc func(*verifierConfig)
|
||||||
|
|
||||||
|
//deprecated: use NewIDTokenVerifier
|
||||||
//NewDefaultVerifier creates `DefaultVerifier` with the given
|
//NewDefaultVerifier creates `DefaultVerifier` with the given
|
||||||
//issuer, clientID, keyset and possible configOptions
|
//issuer, clientID, keyset and possible configOptions
|
||||||
func NewDefaultVerifier(issuer, clientID string, keySet oidc.KeySet, confOpts ...ConfFunc) Verifier {
|
func NewDefaultVerifier(issuer, clientID string, keySet oidc.KeySet, confOpts ...ConfFunc) Verifier {
|
||||||
|
@ -123,17 +123,14 @@ type iatConfig struct {
|
||||||
maxAge time.Duration
|
maxAge time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//deprecated: use oidc.DefaultACRVerifier directly
|
||||||
//DefaultACRVerifier implements `ACRVerifier` returning an error
|
//DefaultACRVerifier implements `ACRVerifier` returning an error
|
||||||
//if non of the provided values matches the acr claim
|
//if non of the provided values matches the acr claim
|
||||||
func DefaultACRVerifier(possibleValues []string) oidc.ACRVerifier {
|
func DefaultACRVerifier(possibleValues []string) oidc.ACRVerifier {
|
||||||
return func(acr string) error {
|
return oidc.DefaultACRVerifier(possibleValues)
|
||||||
if !utils.Contains(possibleValues, acr) {
|
|
||||||
return fmt.Errorf("expected one of: %v, got: %q", possibleValues, acr)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//deprecated: use VerifyTokens(ctx context.Context, accessToken, idTokenString string, v IDTokenVerifier) (*oidc.IDTokenClaims, error) instead
|
||||||
//Verify implements the `Verify` method of the `Verifier` interface
|
//Verify implements the `Verify` method of the `Verifier` interface
|
||||||
//according to https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
|
//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
|
//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)
|
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
|
//Verify implements the `VerifyIDToken` method of the `Verifier` interface
|
||||||
//according to https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
|
//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) {
|
func (v *DefaultVerifier) VerifyIDToken(ctx context.Context, idTokenString string) (*oidc.IDTokenClaims, error) {
|
||||||
|
|
|
@ -3,65 +3,364 @@ package rp
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/caos/oidc/pkg/oidc"
|
"github.com/caos/oidc/pkg/oidc"
|
||||||
|
"github.com/caos/oidc/pkg/oidc/grants"
|
||||||
|
"github.com/caos/oidc/pkg/utils"
|
||||||
|
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
)
|
)
|
||||||
|
|
||||||
//RelayingParty declares the minimal interface for oidc clients
|
//RelayingParty declares the minimal interface for oidc clients
|
||||||
type RelayingParty interface {
|
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 return a standard http client where the token can be used
|
||||||
Client(ctx context.Context, token *oauth2.Token) *http.Client
|
Client(ctx context.Context, token *oauth2.Token) *http.Client
|
||||||
|
|
||||||
//AuthURL returns the authorization endpoint with a given state
|
/*
|
||||||
AuthURL(state string, opts ...AuthURLOpt) string
|
//AuthURL returns the authorization endpoint with a given state
|
||||||
|
AuthURL(state string, opts ...AuthURLOpt) string
|
||||||
|
|
||||||
//AuthURLHandler should implement the AuthURL func as http.HandlerFunc
|
//AuthURLHandler should implement the AuthURL func as http.HandlerFunc
|
||||||
//(redirecting to the auth endpoint)
|
//(redirecting to the auth endpoint)
|
||||||
AuthURLHandler(state string) http.HandlerFunc
|
AuthURLHandler(state string) http.HandlerFunc
|
||||||
|
|
||||||
//CodeExchange implements the OIDC Token Request (oauth2 Authorization Code Grant)
|
//CodeExchange implements the OIDC Token Request (oauth2 Authorization Code Grant)
|
||||||
//returning an `Access Token` and `ID Token Claims`
|
//returning an `Access Token` and `ID Token Claims`
|
||||||
CodeExchange(ctx context.Context, code string, opts ...CodeExchangeOpt) (*oidc.Tokens, error)
|
CodeExchange(ctx context.Context, code string, opts ...CodeExchangeOpt) (*oidc.Tokens, error)
|
||||||
|
|
||||||
//CodeExchangeHandler extends the CodeExchange func,
|
//CodeExchangeHandler extends the CodeExchange func,
|
||||||
//calling the provided callback func on success with additional returned `state`
|
//calling the provided callback func on success with additional returned `state`
|
||||||
CodeExchangeHandler(callback func(http.ResponseWriter, *http.Request, *oidc.Tokens, string)) http.HandlerFunc
|
CodeExchangeHandler(callback func(http.ResponseWriter, *http.Request, *oidc.Tokens, string)) http.HandlerFunc
|
||||||
|
|
||||||
//ClientCredentials implements the oauth2 Client Credentials Grant
|
//ClientCredentials implements the oauth2 Client Credentials Grant
|
||||||
//requesting an `Access Token` for the client itself, without user context
|
//requesting an `Access Token` for the client itself, without user context
|
||||||
ClientCredentials(ctx context.Context, scopes ...string) (*oauth2.Token, error)
|
ClientCredentials(ctx context.Context, scopes ...string) (*oauth2.Token, error)
|
||||||
|
|
||||||
//Introspects calls the Introspect Endpoint
|
//Introspects calls the Introspect Endpoint
|
||||||
//for validating an (access) token
|
//for validating an (access) token
|
||||||
// Introspect(ctx context.Context, token string) (TokenIntrospectResponse, error)
|
// Introspect(ctx context.Context, token string) (TokenIntrospectResponse, error)
|
||||||
|
|
||||||
//Userinfo implements the OIDC Userinfo call
|
//Userinfo implements the OIDC Userinfo call
|
||||||
//returning the info of the user for the requested scopes of an access token
|
//returning the info of the user for the requested scopes of an access token
|
||||||
Userinfo()
|
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`
|
////PasswortGrantRP extends the `RelayingParty` interface with the oauth2 `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.
|
////This interface is separated from the standard `RelayingParty` interface as the `password grant`
|
||||||
type PasswortGrantRP interface {
|
////is part of the oauth2 and therefore OIDC specification, but should only be used when there's no
|
||||||
RelayingParty
|
////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`,
|
var (
|
||||||
//requesting an access token with the users `username` and `password`
|
DefaultErrorHandler = func(w http.ResponseWriter, r *http.Request, errorType string, errorDesc string, state string) {
|
||||||
PasswordGrant(context.Context, string, string) (*oauth2.Token, error)
|
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 {
|
func (rp *relayingParty) OAuthConfig() *oauth2.Config {
|
||||||
ClientID string
|
return rp.oauthConfig
|
||||||
ClientSecret string
|
}
|
||||||
CallbackURL string
|
|
||||||
Issuer string
|
func (rp *relayingParty) IsPKCE() bool {
|
||||||
Scopes []string
|
return rp.pkce
|
||||||
Endpoints oauth2.Endpoint
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
*oauth2.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
type OptionFunc func(RelayingParty)
|
type OptionFunc func(RelayingParty)
|
||||||
|
@ -106,3 +405,22 @@ func WithCodeVerifier(codeVerifier string) CodeExchangeOpt {
|
||||||
return []oauth2.AuthCodeOption{oauth2.SetAuthURLParam("code_verifier", codeVerifier)}
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ import (
|
||||||
type TokenExchangeRP interface {
|
type TokenExchangeRP interface {
|
||||||
RelayingParty
|
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)
|
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
|
//providing an access token in request for a `delegation` token for a given resource / audience
|
||||||
DelegationTokenExchange(context.Context, string, ...tokenexchange.TokenExchangeOption) (*oauth2.Token, error)
|
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)
|
||||||
|
}
|
||||||
|
|
|
@ -9,16 +9,6 @@ import (
|
||||||
"github.com/caos/oidc/pkg/oidc"
|
"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 {
|
type IDTokenVerifier interface {
|
||||||
oidc.Verifier
|
oidc.Verifier
|
||||||
ClientID() string
|
ClientID() string
|
||||||
|
@ -29,63 +19,6 @@ type IDTokenVerifier interface {
|
||||||
MaxAge() time.Duration
|
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
|
//VerifyTokens implement the Token Response Validation as defined in OIDC specification
|
||||||
//https://openid.net/specs/openid-connect-core-1_0.html#TokenResponseValidation
|
//https://openid.net/specs/openid-connect-core-1_0.html#TokenResponseValidation
|
||||||
func VerifyTokens(ctx context.Context, accessToken, idTokenString string, v IDTokenVerifier) (*oidc.IDTokenClaims, error) {
|
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
|
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)
|
||||||
|
}
|
||||||
|
|
|
@ -24,7 +24,8 @@ func GetHashAlgorithm(sigAlgorithm jose.SignatureAlgorithm) (hash.Hash, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func HashString(hash hash.Hash, s string, firstHalf bool) string {
|
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()
|
size := hash.Size()
|
||||||
if firstHalf {
|
if firstHalf {
|
||||||
size = size / 2
|
size = size / 2
|
||||||
|
|
|
@ -25,17 +25,24 @@ type Encoder interface {
|
||||||
Encode(src interface{}, dst map[string][]string) error
|
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)
|
form := make(map[string][]string)
|
||||||
encoder := schema.NewEncoder()
|
encoder := schema.NewEncoder()
|
||||||
if err := encoder.Encode(request, form); err != nil {
|
if err := encoder.Encode(request, form); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if !header {
|
||||||
|
form["client_id"] = []string{clientID}
|
||||||
|
form["client_secret"] = []string{clientSecret}
|
||||||
|
}
|
||||||
body := strings.NewReader(url.Values(form).Encode())
|
body := strings.NewReader(url.Values(form).Encode())
|
||||||
req, err := http.NewRequest("POST", endpoint, body)
|
req, err := http.NewRequest("POST", endpoint, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if header {
|
||||||
|
req.SetBasicAuth(clientID, clientSecret)
|
||||||
|
}
|
||||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
return req, nil
|
return req, nil
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue