881 lines
26 KiB
Go
881 lines
26 KiB
Go
// Package edge_apis_2 edge_apis_2 provides a wrapper around the generated Edge Client and Management APIs improve ease
|
|
// of use.
|
|
package edge_apis
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"github.com/go-openapi/runtime"
|
|
"github.com/go-openapi/strfmt"
|
|
"github.com/go-resty/resty/v2"
|
|
"github.com/golang-jwt/jwt/v5"
|
|
"github.com/openziti/edge-api/rest_client_api_client"
|
|
clientAuth "github.com/openziti/edge-api/rest_client_api_client/authentication"
|
|
clientControllers "github.com/openziti/edge-api/rest_client_api_client/controllers"
|
|
clientApiSession "github.com/openziti/edge-api/rest_client_api_client/current_api_session"
|
|
clientInfo "github.com/openziti/edge-api/rest_client_api_client/informational"
|
|
"github.com/openziti/edge-api/rest_management_api_client"
|
|
manAuth "github.com/openziti/edge-api/rest_management_api_client/authentication"
|
|
manControllers "github.com/openziti/edge-api/rest_management_api_client/controllers"
|
|
manCurApiSession "github.com/openziti/edge-api/rest_management_api_client/current_api_session"
|
|
manInfo "github.com/openziti/edge-api/rest_management_api_client/informational"
|
|
"github.com/openziti/edge-api/rest_model"
|
|
"github.com/openziti/edge-api/rest_util"
|
|
"github.com/openziti/foundation/v2/errorz"
|
|
"github.com/openziti/foundation/v2/stringz"
|
|
"github.com/pkg/errors"
|
|
"github.com/zitadel/oidc/v2/pkg/client/tokenexchange"
|
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
|
"golang.org/x/oauth2"
|
|
"net/http"
|
|
"net/url"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
AuthRequestIdHeader = "auth-request-id"
|
|
TotpRequiredHeader = "totp-required"
|
|
)
|
|
|
|
// AuthEnabledApi is used as a sentinel interface to detect APIs that support authentication and to work around a golang
|
|
// limitation dealing with accessing field of generically typed fields.
|
|
type AuthEnabledApi interface {
|
|
//Authenticate will attempt to issue an authentication request using the provided credentials and http client.
|
|
//These functions act as abstraction around the underlying go-swagger generated client and will use the default
|
|
//http client if not provided.
|
|
Authenticate(credentials Credentials, configTypes []string, httpClient *http.Client) (ApiSession, error)
|
|
SetUseOidc(bool)
|
|
ListControllers() (*rest_model.ControllersList, error)
|
|
GetClientTransportPool() ClientTransportPool
|
|
SetClientTransportPool(ClientTransportPool)
|
|
}
|
|
|
|
type ApiSession interface {
|
|
//GetAccessHeader returns the HTTP header name and value that should be used to represent this ApiSession
|
|
GetAccessHeader() (string, string)
|
|
|
|
//AuthenticateRequest fulfills the interface defined by the OpenAPI libraries to authenticate client HTTP requests
|
|
AuthenticateRequest(request runtime.ClientRequest, _ strfmt.Registry) error
|
|
|
|
//GetToken returns the ApiSessions' token bytes
|
|
GetToken() []byte
|
|
|
|
//GetExpiresAt returns the time when the ApiSession will expire.
|
|
GetExpiresAt() *time.Time
|
|
|
|
//GetAuthQueries returns a list of authentication queries the ApiSession is subjected to
|
|
GetAuthQueries() rest_model.AuthQueryList
|
|
|
|
//GetIdentityName returns the name of the authenticating identity
|
|
GetIdentityName() string
|
|
|
|
//GetIdentityId returns the id of the authenticating identity
|
|
GetIdentityId() string
|
|
|
|
//GetId returns the id of the ApiSession
|
|
GetId() string
|
|
|
|
//RequiresRouterTokenUpdate returns true if the token is a bearer token requires updating on edge router connections.
|
|
RequiresRouterTokenUpdate() bool
|
|
|
|
GetRequestHeaders() http.Header
|
|
}
|
|
|
|
var _ ApiSession = (*ApiSessionLegacy)(nil)
|
|
var _ ApiSession = (*ApiSessionOidc)(nil)
|
|
|
|
// ApiSessionLegacy represents OpenZiti's original authentication API Session Detail, supplied in the `zt-session` header.
|
|
// It has been supplanted by OIDC authentication represented by ApiSessionOidc.
|
|
type ApiSessionLegacy struct {
|
|
Detail *rest_model.CurrentAPISessionDetail
|
|
RequestHeaders http.Header
|
|
}
|
|
|
|
func (a *ApiSessionLegacy) GetRequestHeaders() http.Header {
|
|
return a.RequestHeaders
|
|
}
|
|
|
|
func (a *ApiSessionLegacy) RequiresRouterTokenUpdate() bool {
|
|
return false
|
|
}
|
|
|
|
func (a *ApiSessionLegacy) GetId() string {
|
|
return stringz.OrEmpty(a.Detail.ID)
|
|
}
|
|
|
|
func (a *ApiSessionLegacy) GetIdentityName() string {
|
|
return a.Detail.Identity.Name
|
|
}
|
|
|
|
func (a *ApiSessionLegacy) GetIdentityId() string {
|
|
return stringz.OrEmpty(a.Detail.IdentityID)
|
|
}
|
|
|
|
// GetAccessHeader returns the header and header token value should be used for authentication requests
|
|
func (a *ApiSessionLegacy) GetAccessHeader() (string, string) {
|
|
if a.Detail != nil && a.Detail.Token != nil {
|
|
return "zt-session", *a.Detail.Token
|
|
}
|
|
|
|
return "", ""
|
|
}
|
|
|
|
func (a *ApiSessionLegacy) AuthenticateRequest(request runtime.ClientRequest, _ strfmt.Registry) error {
|
|
if a == nil {
|
|
return errors.New("api session is nil")
|
|
}
|
|
|
|
for h, v := range a.RequestHeaders {
|
|
err := request.SetHeaderParam(h, v...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
//legacy does not support multiple zt-session headers, so we can it sfely
|
|
header, val := a.GetAccessHeader()
|
|
err := request.SetHeaderParam(header, val)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *ApiSessionLegacy) GetToken() []byte {
|
|
if a.Detail != nil && a.Detail.Token != nil {
|
|
return []byte(*a.Detail.Token)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *ApiSessionLegacy) GetAuthQueries() rest_model.AuthQueryList {
|
|
return a.Detail.AuthQueries
|
|
}
|
|
|
|
func (a *ApiSessionLegacy) GetExpiresAt() *time.Time {
|
|
if a.Detail != nil {
|
|
return (*time.Time)(a.Detail.ExpiresAt)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ApiSessionOidc represents an authenticated session backed by OIDC tokens.
|
|
type ApiSessionOidc struct {
|
|
OidcTokens *oidc.Tokens[*oidc.IDTokenClaims]
|
|
RequestHeaders http.Header
|
|
}
|
|
|
|
func (a *ApiSessionOidc) GetRequestHeaders() http.Header {
|
|
return a.RequestHeaders
|
|
}
|
|
|
|
func (a *ApiSessionOidc) RequiresRouterTokenUpdate() bool {
|
|
return true
|
|
}
|
|
|
|
func (a *ApiSessionOidc) GetAccessClaims() (*ApiAccessClaims, error) {
|
|
claims := &ApiAccessClaims{}
|
|
|
|
parser := jwt.NewParser()
|
|
_, _, err := parser.ParseUnverified(a.OidcTokens.AccessToken, claims)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return claims, nil
|
|
}
|
|
|
|
func (a *ApiSessionOidc) GetId() string {
|
|
claims, err := a.GetAccessClaims()
|
|
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
|
|
return claims.ApiSessionId
|
|
}
|
|
|
|
func (a *ApiSessionOidc) GetIdentityName() string {
|
|
return a.OidcTokens.IDTokenClaims.Name
|
|
}
|
|
|
|
func (a *ApiSessionOidc) GetIdentityId() string {
|
|
return a.OidcTokens.IDTokenClaims.Subject
|
|
}
|
|
|
|
// GetAccessHeader returns the header and header token value should be used for authentication requests
|
|
func (a *ApiSessionOidc) GetAccessHeader() (string, string) {
|
|
if a.OidcTokens != nil {
|
|
return "authorization", "Bearer " + a.OidcTokens.AccessToken
|
|
}
|
|
|
|
return "", ""
|
|
}
|
|
|
|
func (a *ApiSessionOidc) AuthenticateRequest(request runtime.ClientRequest, _ strfmt.Registry) error {
|
|
if a == nil {
|
|
return errors.New("api session is nil")
|
|
}
|
|
|
|
if a.RequestHeaders == nil {
|
|
a.RequestHeaders = http.Header{}
|
|
}
|
|
|
|
//multiple Authorization headers are allowed, obtain all auth header candidates
|
|
primaryAuthHeader, primaryAuthValue := a.GetAccessHeader()
|
|
altAuthValues := a.RequestHeaders.Get(primaryAuthHeader)
|
|
|
|
authValues := []string{primaryAuthValue}
|
|
|
|
if len(altAuthValues) > 0 {
|
|
authValues = append(authValues, altAuthValues)
|
|
}
|
|
|
|
//set request headers
|
|
for h, v := range a.RequestHeaders {
|
|
err := request.SetHeaderParam(h, v...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
//restore auth headers
|
|
err := request.SetHeaderParam(primaryAuthHeader, authValues...)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *ApiSessionOidc) GetToken() []byte {
|
|
if a.OidcTokens != nil && a.OidcTokens.AccessToken != "" {
|
|
return []byte(a.OidcTokens.AccessToken)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *ApiSessionOidc) GetAuthQueries() rest_model.AuthQueryList {
|
|
//todo convert JWT auth queries to rest_model.AuthQueryList
|
|
return nil
|
|
}
|
|
|
|
func (a *ApiSessionOidc) GetExpiresAt() *time.Time {
|
|
if a.OidcTokens != nil {
|
|
return &a.OidcTokens.Expiry
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var _ AuthEnabledApi = (*ZitiEdgeManagement)(nil)
|
|
|
|
// ZitiEdgeManagement is an alias of the go-swagger generated client that allows this package to add additional
|
|
// functionality to the alias type to implement the AuthEnabledApi interface.
|
|
type ZitiEdgeManagement struct {
|
|
*rest_management_api_client.ZitiEdgeManagement
|
|
// useOidc tracks if OIDC auth should be used
|
|
useOidc bool
|
|
|
|
// useOidcExplicitlySet signals if useOidc was set from an external caller and should be used as is
|
|
useOidcExplicitlySet bool
|
|
|
|
// oidcDynamicallyEnabled will cause the client to check the controller for OIDC support and use if possible as long as useOidc was not explicitly set
|
|
oidcDynamicallyEnabled bool //currently defaults false till HA release
|
|
|
|
versionOnce sync.Once
|
|
versionInfo *rest_model.Version
|
|
|
|
TotpCallback func(chan string)
|
|
ClientTransportPool ClientTransportPool
|
|
}
|
|
|
|
func (self *ZitiEdgeManagement) SetClientTransportPool(transportPool ClientTransportPool) {
|
|
self.ClientTransportPool = transportPool
|
|
}
|
|
|
|
func (self *ZitiEdgeManagement) GetClientTransportPool() ClientTransportPool {
|
|
return self.ClientTransportPool
|
|
}
|
|
|
|
func (self *ZitiEdgeManagement) ListControllers() (*rest_model.ControllersList, error) {
|
|
params := manControllers.NewListControllersParams()
|
|
resp, err := self.Controllers.ListControllers(params, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &resp.GetPayload().Data, nil
|
|
}
|
|
|
|
func (self *ZitiEdgeManagement) Authenticate(credentials Credentials, configTypes []string, httpClient *http.Client) (ApiSession, error) {
|
|
self.versionOnce.Do(func() {
|
|
if self.useOidcExplicitlySet {
|
|
return
|
|
}
|
|
|
|
if self.oidcDynamicallyEnabled {
|
|
versionParams := manInfo.NewListVersionParams()
|
|
|
|
versionResp, _ := self.Informational.ListVersion(versionParams)
|
|
|
|
if versionResp != nil {
|
|
self.versionInfo = versionResp.Payload.Data
|
|
self.useOidc = stringz.Contains(self.versionInfo.Capabilities, string(rest_model.CapabilitiesOIDCAUTH))
|
|
}
|
|
} else {
|
|
self.useOidc = false
|
|
}
|
|
})
|
|
|
|
if self.useOidc {
|
|
return self.oidcAuth(credentials, configTypes, httpClient)
|
|
}
|
|
|
|
return self.legacyAuth(credentials, configTypes, httpClient)
|
|
}
|
|
|
|
func (self *ZitiEdgeManagement) legacyAuth(credentials Credentials, configTypes []string, httpClient *http.Client) (ApiSession, error) {
|
|
params := manAuth.NewAuthenticateParams()
|
|
params.Auth = credentials.Payload()
|
|
params.Method = credentials.Method()
|
|
params.Auth.ConfigTypes = append(params.Auth.ConfigTypes, configTypes...)
|
|
|
|
certs := credentials.TlsCerts()
|
|
if len(certs) != 0 {
|
|
if transport, ok := httpClient.Transport.(*http.Transport); ok {
|
|
transport.TLSClientConfig.Certificates = certs
|
|
transport.CloseIdleConnections()
|
|
}
|
|
}
|
|
|
|
resp, err := self.Authentication.Authenticate(params, getClientAuthInfoOp(credentials, httpClient))
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &ApiSessionLegacy{
|
|
Detail: resp.GetPayload().Data,
|
|
RequestHeaders: credentials.GetRequestHeaders()}, err
|
|
}
|
|
|
|
func (self *ZitiEdgeManagement) oidcAuth(credentials Credentials, configTypeOverrides []string, httpClient *http.Client) (ApiSession, error) {
|
|
return oidcAuth(self.ClientTransportPool, credentials, configTypeOverrides, httpClient, self.TotpCallback)
|
|
}
|
|
|
|
func (self *ZitiEdgeManagement) SetUseOidc(use bool) {
|
|
self.useOidcExplicitlySet = true
|
|
self.useOidc = use
|
|
}
|
|
|
|
func (self *ZitiEdgeManagement) SetAllowOidcDynamicallyEnabled(allow bool) {
|
|
self.oidcDynamicallyEnabled = allow
|
|
}
|
|
|
|
func (self *ZitiEdgeManagement) RefreshApiSession(apiSession ApiSession, httpClient *http.Client) (ApiSession, error) {
|
|
switch s := apiSession.(type) {
|
|
case *ApiSessionLegacy:
|
|
params := manCurApiSession.NewGetCurrentAPISessionParams()
|
|
_, err := self.CurrentAPISession.GetCurrentAPISession(params, s)
|
|
|
|
if err != nil {
|
|
return nil, rest_util.WrapErr(err)
|
|
}
|
|
|
|
return s, nil
|
|
case *ApiSessionOidc:
|
|
tokens, err := self.ExchangeTokens(s.OidcTokens, httpClient)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &ApiSessionOidc{
|
|
OidcTokens: tokens,
|
|
RequestHeaders: apiSession.GetRequestHeaders(),
|
|
}, nil
|
|
}
|
|
|
|
return nil, errors.New("api session does not have any tokens")
|
|
}
|
|
|
|
func (self *ZitiEdgeManagement) ExchangeTokens(curTokens *oidc.Tokens[*oidc.IDTokenClaims], httpClient *http.Client) (*oidc.Tokens[*oidc.IDTokenClaims], error) {
|
|
return exchangeTokens(self.ClientTransportPool, curTokens, httpClient)
|
|
}
|
|
|
|
var _ AuthEnabledApi = (*ZitiEdgeClient)(nil)
|
|
|
|
// ZitiEdgeClient is an alias of the go-swagger generated client that allows this package to add additional
|
|
// functionality to the alias type to implement the AuthEnabledApi interface.
|
|
type ZitiEdgeClient struct {
|
|
*rest_client_api_client.ZitiEdgeClient
|
|
// useOidc tracks if OIDC auth should be used
|
|
useOidc bool
|
|
|
|
// useOidcExplicitlySet signals if useOidc was set from an external caller and should be used as is
|
|
useOidcExplicitlySet bool
|
|
|
|
// oidcDynamicallyEnabled will cause the client to check the controller for OIDC support and use if possible as long as useOidc was not explicitly set.
|
|
oidcDynamicallyEnabled bool //currently defaults false till HA release
|
|
|
|
versionInfo *rest_model.Version
|
|
versionOnce sync.Once
|
|
|
|
TotpCallback func(chan string)
|
|
ClientTransportPool ClientTransportPool
|
|
}
|
|
|
|
func (self *ZitiEdgeClient) GetClientTransportPool() ClientTransportPool {
|
|
return self.ClientTransportPool
|
|
}
|
|
|
|
func (self *ZitiEdgeClient) SetClientTransportPool(transportPool ClientTransportPool) {
|
|
self.ClientTransportPool = transportPool
|
|
}
|
|
|
|
func (self *ZitiEdgeClient) ListControllers() (*rest_model.ControllersList, error) {
|
|
params := clientControllers.NewListControllersParams()
|
|
resp, err := self.Controllers.ListControllers(params, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &resp.GetPayload().Data, nil
|
|
}
|
|
|
|
func (self *ZitiEdgeClient) Authenticate(credentials Credentials, configTypesOverrides []string, httpClient *http.Client) (ApiSession, error) {
|
|
self.versionOnce.Do(func() {
|
|
if self.useOidcExplicitlySet {
|
|
return
|
|
}
|
|
|
|
if self.oidcDynamicallyEnabled {
|
|
versionParams := clientInfo.NewListVersionParams()
|
|
|
|
versionResp, _ := self.Informational.ListVersion(versionParams)
|
|
|
|
if versionResp != nil {
|
|
self.versionInfo = versionResp.Payload.Data
|
|
self.useOidc = stringz.Contains(self.versionInfo.Capabilities, string(rest_model.CapabilitiesOIDCAUTH))
|
|
}
|
|
} else {
|
|
self.useOidc = false
|
|
}
|
|
})
|
|
|
|
if self.useOidc {
|
|
return self.oidcAuth(credentials, configTypesOverrides, httpClient)
|
|
}
|
|
|
|
return self.legacyAuth(credentials, configTypesOverrides, httpClient)
|
|
}
|
|
|
|
func (self *ZitiEdgeClient) legacyAuth(credentials Credentials, configTypes []string, httpClient *http.Client) (ApiSession, error) {
|
|
params := clientAuth.NewAuthenticateParams()
|
|
params.Auth = credentials.Payload()
|
|
params.Method = credentials.Method()
|
|
params.Auth.ConfigTypes = append(params.Auth.ConfigTypes, configTypes...)
|
|
|
|
certs := credentials.TlsCerts()
|
|
if len(certs) != 0 {
|
|
if transport, ok := httpClient.Transport.(*http.Transport); ok {
|
|
transport.TLSClientConfig.Certificates = certs
|
|
transport.CloseIdleConnections()
|
|
}
|
|
}
|
|
|
|
resp, err := self.Authentication.Authenticate(params, getClientAuthInfoOp(credentials, httpClient))
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &ApiSessionLegacy{Detail: resp.GetPayload().Data, RequestHeaders: credentials.GetRequestHeaders()}, err
|
|
}
|
|
|
|
func (self *ZitiEdgeClient) oidcAuth(credentials Credentials, configTypeOverrides []string, httpClient *http.Client) (ApiSession, error) {
|
|
return oidcAuth(self.ClientTransportPool, credentials, configTypeOverrides, httpClient, self.TotpCallback)
|
|
}
|
|
|
|
func (self *ZitiEdgeClient) SetUseOidc(use bool) {
|
|
self.useOidcExplicitlySet = true
|
|
self.useOidc = use
|
|
}
|
|
|
|
func (self *ZitiEdgeClient) SetAllowOidcDynamicallyEnabled(allow bool) {
|
|
self.oidcDynamicallyEnabled = allow
|
|
}
|
|
|
|
func (self *ZitiEdgeClient) RefreshApiSession(apiSession ApiSession, httpClient *http.Client) (ApiSession, error) {
|
|
switch s := apiSession.(type) {
|
|
case *ApiSessionLegacy:
|
|
params := clientApiSession.NewGetCurrentAPISessionParams()
|
|
newApiSessionDetail, err := self.CurrentAPISession.GetCurrentAPISession(params, s)
|
|
|
|
if err != nil {
|
|
return nil, rest_util.WrapErr(err)
|
|
}
|
|
|
|
newApiSession := &ApiSessionLegacy{
|
|
Detail: newApiSessionDetail.Payload.Data,
|
|
RequestHeaders: apiSession.GetRequestHeaders(),
|
|
}
|
|
|
|
return newApiSession, nil
|
|
case *ApiSessionOidc:
|
|
tokens, err := self.ExchangeTokens(s.OidcTokens, httpClient)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &ApiSessionOidc{
|
|
OidcTokens: tokens,
|
|
RequestHeaders: apiSession.GetRequestHeaders(),
|
|
}, nil
|
|
}
|
|
|
|
return nil, errors.New("api session does not have any tokens")
|
|
}
|
|
|
|
func (self *ZitiEdgeClient) ExchangeTokens(curTokens *oidc.Tokens[*oidc.IDTokenClaims], httpClient *http.Client) (*oidc.Tokens[*oidc.IDTokenClaims], error) {
|
|
return exchangeTokens(self.ClientTransportPool, curTokens, httpClient)
|
|
}
|
|
|
|
func exchangeTokens(clientTransportPool ClientTransportPool, curTokens *oidc.Tokens[*oidc.IDTokenClaims], client *http.Client) (*oidc.Tokens[*oidc.IDTokenClaims], error) {
|
|
subjectToken := curTokens.RefreshToken
|
|
subjectTokenType := oidc.RefreshTokenType
|
|
|
|
// if subjectToken is "", then we don't have a refresh token, attempt to exchange a non-expired access token
|
|
if subjectToken == "" {
|
|
if curTokens.Expiry.Before(time.Now()) {
|
|
return nil, errors.New("cannot exchange token: refresh token not found, access token expired")
|
|
}
|
|
|
|
if curTokens.AccessToken == "" {
|
|
return nil, errors.New("cannot exchange token: refresh token not found, access token not found")
|
|
}
|
|
subjectToken = curTokens.AccessToken
|
|
subjectTokenType = oidc.AccessTokenType
|
|
}
|
|
|
|
var outTokens *oidc.Tokens[*oidc.IDTokenClaims]
|
|
|
|
_, err := clientTransportPool.TryTransportForF(func(transport *ApiClientTransport) (any, error) {
|
|
apiHost := transport.ApiUrl.Host
|
|
issuer := "https://" + apiHost + "/oidc"
|
|
tokenEndpoint := "https://" + apiHost + "/oidc/oauth/token"
|
|
|
|
te, err := tokenexchange.NewTokenExchangerClientCredentials(issuer, "native", "", tokenexchange.WithHTTPClient(client), tokenexchange.WithStaticTokenEndpoint(issuer, tokenEndpoint))
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var tokenResponse *oidc.TokenExchangeResponse
|
|
|
|
now := time.Now()
|
|
|
|
switch subjectTokenType {
|
|
case oidc.RefreshTokenType:
|
|
tokenResponse, err = tokenexchange.ExchangeToken(te, subjectToken, subjectTokenType, "", "", nil, nil, nil, oidc.RefreshTokenType)
|
|
case oidc.AccessTokenType:
|
|
tokenResponse, err = tokenexchange.ExchangeToken(te, subjectToken, subjectTokenType, "", "", nil, nil, nil, oidc.AccessTokenType)
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
idResp, err := tokenexchange.ExchangeToken(te, subjectToken, subjectTokenType, "", "", nil, nil, nil, oidc.IDTokenType)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
idClaims := &IdClaims{}
|
|
|
|
//access token is used to hold id token per zitadel comments
|
|
_, _, err = jwt.NewParser().ParseUnverified(idResp.AccessToken, idClaims)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
outTokens = &oidc.Tokens[*oidc.IDTokenClaims]{
|
|
Token: &oauth2.Token{
|
|
AccessToken: tokenResponse.AccessToken,
|
|
TokenType: tokenResponse.TokenType,
|
|
RefreshToken: tokenResponse.RefreshToken,
|
|
Expiry: now.Add(time.Second * time.Duration(tokenResponse.ExpiresIn)),
|
|
},
|
|
IDTokenClaims: &idClaims.IDTokenClaims,
|
|
IDToken: idResp.AccessToken, //access token field is used to hold id token per zitadel comments
|
|
}
|
|
|
|
return outTokens, nil
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return outTokens, nil
|
|
}
|
|
|
|
type authPayload struct {
|
|
*rest_model.Authenticate
|
|
AuthRequestId string `json:"id"`
|
|
}
|
|
|
|
type totpCodePayload struct {
|
|
rest_model.MfaCode
|
|
AuthRequestId string `json:"id"`
|
|
}
|
|
|
|
func (a *authPayload) toValues() url.Values {
|
|
result := url.Values{
|
|
"id": []string{a.AuthRequestId},
|
|
"password": []string{string(a.Password)},
|
|
"username": []string{string(a.Username)},
|
|
"configTypes": a.ConfigTypes,
|
|
"envArch": []string{a.EnvInfo.Arch},
|
|
"envOs": []string{a.EnvInfo.Os},
|
|
"envOsRelease": []string{a.EnvInfo.OsRelease},
|
|
"envOsVersion": []string{a.EnvInfo.OsVersion},
|
|
"sdkAppID": []string{a.SdkInfo.AppID},
|
|
"sdkAppVersion": []string{a.SdkInfo.AppVersion},
|
|
"sdkBranch": []string{a.SdkInfo.Branch},
|
|
"sdkRevision": []string{a.SdkInfo.Revision},
|
|
"sdkType": []string{a.SdkInfo.Type},
|
|
"sdkVersion": []string{a.SdkInfo.Version},
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func oidcAuth(clientTransportPool ClientTransportPool, credentials Credentials, configTypeOverrides []string, httpClient *http.Client, totpCallback func(chan string)) (ApiSession, error) {
|
|
payload := &authPayload{
|
|
Authenticate: credentials.Payload(),
|
|
}
|
|
method := credentials.Method()
|
|
|
|
if configTypeOverrides != nil {
|
|
payload.ConfigTypes = configTypeOverrides
|
|
}
|
|
|
|
certs := credentials.TlsCerts()
|
|
|
|
if len(certs) != 0 {
|
|
if transport, ok := httpClient.Transport.(*http.Transport); ok {
|
|
transport.TLSClientConfig.Certificates = certs
|
|
transport.CloseIdleConnections()
|
|
}
|
|
}
|
|
|
|
var outTokens *oidc.Tokens[*oidc.IDTokenClaims]
|
|
|
|
_, err := clientTransportPool.TryTransportForF(func(transport *ApiClientTransport) (any, error) {
|
|
rpServer, err := newLocalRpServer(transport.ApiUrl.Host, method)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
rpServer.Start()
|
|
defer rpServer.Stop()
|
|
|
|
client := resty.NewWithClient(httpClient)
|
|
apiHost := transport.ApiUrl.Hostname()
|
|
|
|
client.SetRedirectPolicy(resty.DomainCheckRedirectPolicy("127.0.0.1", "localhost", apiHost))
|
|
resp, err := client.R().Get(rpServer.LoginUri)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if resp.StatusCode() != http.StatusOK {
|
|
return nil, fmt.Errorf("local rp login response is expected to be HTTP status %d got %d with body: %s", http.StatusOK, resp.StatusCode(), resp.Body())
|
|
}
|
|
payload.AuthRequestId = resp.Header().Get(AuthRequestIdHeader)
|
|
|
|
if payload.AuthRequestId == "" {
|
|
return nil, errors.New("could not find auth request id header")
|
|
}
|
|
|
|
opLoginUri := "https://" + resp.RawResponse.Request.URL.Host + "/oidc/login/" + method
|
|
totpUri := "https://" + resp.RawResponse.Request.URL.Host + "/oidc/login/totp"
|
|
|
|
formData := payload.toValues()
|
|
|
|
req := client.R()
|
|
clientRequest := asClientRequest(req, client)
|
|
|
|
err = credentials.AuthenticateRequest(clientRequest, strfmt.Default)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
resp, err = req.SetFormDataFromValues(formData).Post(opLoginUri)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if resp.StatusCode() != http.StatusOK {
|
|
return nil, fmt.Errorf("remote op login response is expected to be HTTP status %d got %d with body: %s", http.StatusOK, resp.StatusCode(), resp.Body())
|
|
}
|
|
|
|
authRequestId := payload.AuthRequestId
|
|
totpRequiredHeader := resp.Header().Get(TotpRequiredHeader)
|
|
totpRequired := totpRequiredHeader != ""
|
|
totpCode := ""
|
|
|
|
if totpRequired {
|
|
|
|
if totpCallback == nil {
|
|
return nil, errors.New("totp is required but not totp callback was defined")
|
|
}
|
|
codeChan := make(chan string)
|
|
go totpCallback(codeChan)
|
|
|
|
select {
|
|
case code := <-codeChan:
|
|
totpCode = code
|
|
case <-time.After(30 * time.Minute):
|
|
return nil, fmt.Errorf("timedout waiting for totpT callback")
|
|
}
|
|
|
|
resp, err = client.R().SetBody(&totpCodePayload{
|
|
MfaCode: rest_model.MfaCode{
|
|
Code: &totpCode,
|
|
},
|
|
AuthRequestId: authRequestId,
|
|
}).Post(totpUri)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if resp.StatusCode() != http.StatusOK {
|
|
apiErr := &errorz.ApiError{}
|
|
err = json.Unmarshal(resp.Body(), apiErr)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not verify TOTP MFA code recieved %d - could not parse body: %s", resp.StatusCode(), string(resp.Body()))
|
|
}
|
|
|
|
return nil, apiErr
|
|
}
|
|
}
|
|
|
|
var tokens *oidc.Tokens[*oidc.IDTokenClaims]
|
|
select {
|
|
case tokens = <-rpServer.TokenChan:
|
|
case <-time.After(30 * time.Minute):
|
|
}
|
|
|
|
if tokens == nil {
|
|
return nil, errors.New("authentication did not complete, received nil tokens")
|
|
}
|
|
outTokens = tokens
|
|
|
|
return nil, nil
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &ApiSessionOidc{
|
|
OidcTokens: outTokens,
|
|
RequestHeaders: credentials.GetRequestHeaders(),
|
|
}, nil
|
|
}
|
|
|
|
// restyClientRequest is meant to mimic open api's client request which is a combination
|
|
// of resty's request and client.
|
|
type restyClientRequest struct {
|
|
restyRequest *resty.Request
|
|
restyClient *resty.Client
|
|
}
|
|
|
|
func (r *restyClientRequest) SetHeaderParam(s string, s2 ...string) error {
|
|
r.restyRequest.Header[s] = s2
|
|
return nil
|
|
}
|
|
|
|
func (r *restyClientRequest) GetHeaderParams() http.Header {
|
|
return r.restyRequest.Header
|
|
}
|
|
|
|
func (r *restyClientRequest) SetQueryParam(s string, s2 ...string) error {
|
|
r.restyRequest.QueryParam[s] = s2
|
|
return nil
|
|
}
|
|
|
|
func (r *restyClientRequest) SetFormParam(s string, s2 ...string) error {
|
|
r.restyRequest.FormData[s] = s2
|
|
return nil
|
|
}
|
|
|
|
func (r *restyClientRequest) SetPathParam(s string, s2 string) error {
|
|
r.restyRequest.PathParams[s] = s2
|
|
return nil
|
|
}
|
|
|
|
func (r *restyClientRequest) GetQueryParams() url.Values {
|
|
return r.restyRequest.QueryParam
|
|
}
|
|
|
|
func (r *restyClientRequest) SetFileParam(s string, closer ...runtime.NamedReadCloser) error {
|
|
for _, curCloser := range closer {
|
|
r.restyRequest.SetFileReader(s, curCloser.Name(), curCloser)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *restyClientRequest) SetBodyParam(i interface{}) error {
|
|
r.restyRequest.SetBody(i)
|
|
return nil
|
|
}
|
|
|
|
func (r *restyClientRequest) SetTimeout(duration time.Duration) error {
|
|
r.restyClient.SetTimeout(duration)
|
|
return nil
|
|
}
|
|
|
|
func (r *restyClientRequest) GetMethod() string {
|
|
return r.restyRequest.Method
|
|
}
|
|
|
|
func (r *restyClientRequest) GetPath() string {
|
|
return r.restyRequest.URL
|
|
}
|
|
|
|
func (r *restyClientRequest) GetBody() []byte {
|
|
return r.restyRequest.Body.([]byte)
|
|
}
|
|
|
|
func (r *restyClientRequest) GetBodyParam() interface{} {
|
|
return r.restyRequest.Body
|
|
}
|
|
|
|
func (r *restyClientRequest) GetFileParam() map[string][]runtime.NamedReadCloser {
|
|
return nil
|
|
}
|
|
|
|
func asClientRequest(request *resty.Request, client *resty.Client) runtime.ClientRequest {
|
|
return &restyClientRequest{request, client}
|
|
}
|