You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
102 lines
3.3 KiB
102 lines
3.3 KiB
package toolbox
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
errorhandler "github.com/cloudtrust/common-service/errors"
|
|
"github.com/cloudtrust/keycloak-client/v3"
|
|
)
|
|
|
|
// OidcTokenProvider provides OIDC tokens
|
|
type OidcTokenProvider interface {
|
|
ProvideToken(ctx context.Context) (string, error)
|
|
}
|
|
|
|
type oidcToken struct {
|
|
AccessToken string `json:"access_token,omitempty"`
|
|
ExpiresIn int64 `json:"expires_in,omitempty"`
|
|
RefreshToken string `json:"refresh_token,omitempty"`
|
|
RefreshExpiresIn int64 `json:"refresh_expires_in,omitempty"`
|
|
TokenType string `json:"token_type,omitempty"`
|
|
NotBeforePolicy int `json:"not-before-policy,omitempty"`
|
|
SessionState string `json:"session_state,omitempty"`
|
|
Scope string `json:"scope,omitempty"`
|
|
}
|
|
|
|
type oidcTokenProvider struct {
|
|
timeout time.Duration
|
|
tokenURL string
|
|
reqBody string
|
|
logger Logger
|
|
oidcToken oidcToken
|
|
validUntil int64
|
|
}
|
|
|
|
const (
|
|
// Max processing delay: let's assume that the user of OidcTokenProvider will have a maximum of 5 seconds to use the provided OIDC token
|
|
maxProcessingDelay = int64(5)
|
|
)
|
|
|
|
// NewOidcTokenProvider creates an OidcTokenProvider
|
|
func NewOidcTokenProvider(config keycloak.Config, realm, username, password, clientID string, logger Logger) OidcTokenProvider {
|
|
var urls = strings.Split(config.AddrTokenProvider, " ")
|
|
var keycloakPublicURL = urls[0]
|
|
|
|
var tokenURL = fmt.Sprintf("%s/auth/realms/%s/protocol/openid-connect/token", keycloakPublicURL, realm)
|
|
// If needed, can add &client_secret={secret}
|
|
var body = fmt.Sprintf("grant_type=password&client_id=%s&username=%s&password=%s",
|
|
url.QueryEscape(clientID), url.QueryEscape(username), url.QueryEscape(password))
|
|
|
|
return &oidcTokenProvider{
|
|
timeout: config.Timeout,
|
|
tokenURL: tokenURL,
|
|
reqBody: body,
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
func (o *oidcTokenProvider) ProvideToken(ctx context.Context) (string, error) {
|
|
if o.validUntil+maxProcessingDelay > time.Now().Unix() {
|
|
return o.oidcToken.AccessToken, nil
|
|
}
|
|
|
|
var mimeType = "application/x-www-form-urlencoded"
|
|
var httpClient = http.Client{
|
|
Timeout: o.timeout,
|
|
}
|
|
var resp, err = httpClient.Post(o.tokenURL, mimeType, strings.NewReader(o.reqBody))
|
|
if err != nil {
|
|
o.logger.Warn(ctx, "msg", err.Error())
|
|
return "", errorhandler.CreateInternalServerError("unexpected.httpResponse")
|
|
}
|
|
if err == nil && resp.StatusCode == http.StatusUnauthorized {
|
|
o.logger.Warn(ctx, "msg", "Technical user credentials are invalid")
|
|
return "", errorhandler.Error{
|
|
Status: http.StatusUnauthorized,
|
|
Message: errorhandler.GetEmitter() + ".unauthorized",
|
|
}
|
|
}
|
|
if resp.StatusCode >= 400 || resp.Body == http.NoBody || resp.Body == nil {
|
|
o.logger.Warn(ctx, "msg", fmt.Sprintf("Unexpected behavior: unexpected http status (%d) or response has no body", resp.StatusCode))
|
|
return "", errorhandler.CreateInternalServerError("unexpected.httpResponse")
|
|
}
|
|
|
|
buf := new(bytes.Buffer)
|
|
_, _ = buf.ReadFrom(resp.Body)
|
|
|
|
err = json.Unmarshal(buf.Bytes(), &o.oidcToken)
|
|
if err != nil {
|
|
o.logger.Warn(ctx, "msg", fmt.Sprintf("Can't deserialize token. JSON: %s", buf.String()))
|
|
return "", errorhandler.CreateInternalServerError("unexpected.oidcToken")
|
|
}
|
|
o.validUntil = time.Now().Unix() + o.oidcToken.ExpiresIn
|
|
|
|
return o.oidcToken.AccessToken, nil
|
|
}
|
|
|