From fbe0cf006f73a23aed838ead5aa52a09c454cfcd Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 12 Dec 2023 19:35:45 -0500 Subject: [PATCH 1/3] add max ttl to renew and login api responses --- .../controllers/v1/universalAuthController.ts | 101 +++++++++--------- 1 file changed, 50 insertions(+), 51 deletions(-) diff --git a/backend/src/controllers/v1/universalAuthController.ts b/backend/src/controllers/v1/universalAuthController.ts index e22c4e686..750c35639 100644 --- a/backend/src/controllers/v1/universalAuthController.ts +++ b/backend/src/controllers/v1/universalAuthController.ts @@ -3,28 +3,28 @@ import { Types } from "mongoose"; import jwt from "jsonwebtoken"; import crypto from "crypto"; import bcrypt from "bcrypt"; -import { - IIdentity, - IIdentityTrustedIp, - IIdentityUniversalAuthClientSecret, - Identity, - IdentityAccessToken, - IdentityAuthMethod, - IdentityMembershipOrg, - IdentityUniversalAuth, - IdentityUniversalAuthClientSecret, +import { + IIdentity, + IIdentityTrustedIp, + IIdentityUniversalAuthClientSecret, + Identity, + IdentityAccessToken, + IdentityAuthMethod, + IdentityMembershipOrg, + IdentityUniversalAuth, + IdentityUniversalAuthClientSecret, } from "../../models"; import { createToken } from "../../helpers/auth"; import { AuthTokenType } from "../../variables"; -import { - BadRequestError, - ForbiddenRequestError, - ResourceNotFoundError, - UnauthorizedRequestError +import { + BadRequestError, + ForbiddenRequestError, + ResourceNotFoundError, + UnauthorizedRequestError } from "../../utils/errors"; import { - getAuthSecret, - getSaltRounds + getAuthSecret, + getSaltRounds } from "../../config"; import { ActorType, EventType, IRole } from "../../ee/models"; import { validateRequest } from "../../helpers/validation"; @@ -32,12 +32,12 @@ import * as reqValidator from "../../validation/auth"; import { checkIPAgainstBlocklist, extractIPDetails, isValidIpOrCidr } from "../../utils/ip"; import { getUserAgentType } from "../../utils/posthog"; import { EEAuditLogService, EELicenseService } from "../../ee/services"; -import { - OrgPermissionActions, - OrgPermissionSubjects, - getAuthDataOrgPermissions, - getOrgRolePermissions, - isAtLeastAsPrivilegedOrg +import { + OrgPermissionActions, + OrgPermissionSubjects, + getAuthDataOrgPermissions, + getOrgRolePermissions, + isAtLeastAsPrivilegedOrg } from "../../ee/services/RoleService"; import { ForbiddenError } from "@casl/ability"; @@ -53,7 +53,7 @@ const packageUniversalAuthClientSecretData = (identityUniversalAuthClientSecret: createdAt: identityUniversalAuthClientSecret.createdAt, updatedAt: identityUniversalAuthClientSecret.updatedAt }); - + /** * Renews an access token by its TTL * @param req @@ -86,9 +86,6 @@ export const renewAccessToken = async (req: Request, res: Response) => { createdAt: accessTokenCreatedAt } = identityAccessToken; - if (accessTokenTTL === accessTokenMaxTTL) throw UnauthorizedRequestError({ - message: "Failed to renew non-renewable access token" - }); // ttl check if (accessTokenTTL > 0) { @@ -141,6 +138,7 @@ export const renewAccessToken = async (req: Request, res: Response) => { return res.status(200).send({ accessToken, expiresIn: identityAccessToken.accessTokenTTL, + accessTokenMaxTTL: identityAccessToken.accessTokenMaxTTL, tokenType: "Bearer" }); } @@ -162,7 +160,7 @@ export const loginIdentityUniversalAuth = async (req: Request, res: Response) => const identityUniversalAuth = await IdentityUniversalAuth.findOne({ clientId }).populate<{ identity: IIdentity }>("identity"); - + if (!identityUniversalAuth) throw UnauthorizedRequestError(); checkIPAgainstBlocklist({ @@ -237,16 +235,16 @@ export const loginIdentityUniversalAuth = async (req: Request, res: Response) => // increment usage count by 1 await IdentityUniversalAuthClientSecret - .findByIdAndUpdate( - validatedClientSecretDatum._id, - { - clientSecretLastUsedAt: new Date(), - $inc: { clientSecretNumUses: 1 } - }, - { - new: true - } - ); + .findByIdAndUpdate( + validatedClientSecretDatum._id, + { + clientSecretLastUsedAt: new Date(), + $inc: { clientSecretNumUses: 1 } + }, + { + new: true + } + ); const identityAccessToken = await new IdentityAccessToken({ identity: identityUniversalAuth.identity, @@ -300,7 +298,8 @@ export const loginIdentityUniversalAuth = async (req: Request, res: Response) => return res.status(200).send({ accessToken, expiresIn: identityUniversalAuth.accessTokenTTL, - tokenType: "Bearer" + accessTokenMaxTTL: identityUniversalAuth.accessTokenMaxTTL, + tokenType: "Bearer", }); } @@ -328,7 +327,7 @@ export const addIdentityUniversalAuth = async (req: Request, res: Response) => { if (!identityMembershipOrg) throw ResourceNotFoundError({ message: `Failed to find identity with id ${identityId}` }); - + if (identityMembershipOrg.identity?.authMethod) throw BadRequestError({ message: "Failed to add universal auth to already-configured identity" }); @@ -377,7 +376,7 @@ export const addIdentityUniversalAuth = async (req: Request, res: Response) => { return extractIPDetails(accessTokenTrustedIp.ipAddress); }); - + const identityUniversalAuth = await new IdentityUniversalAuth({ identity: identityMembershipOrg.identity._id, clientId: crypto.randomUUID(), @@ -387,7 +386,7 @@ export const addIdentityUniversalAuth = async (req: Request, res: Response) => { accessTokenNumUsesLimit, accessTokenTrustedIps: reformattedAccessTokenTrustedIps, }).save(); - + await Identity.findByIdAndUpdate( identityMembershipOrg.identity._id, { @@ -439,7 +438,7 @@ export const updateIdentityUniversalAuth = async (req: Request, res: Response) = if (!identityMembershipOrg) throw ResourceNotFoundError({ message: `Failed to find identity with id ${identityId}` }); - + if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.UNIVERSAL_AUTH) throw BadRequestError({ message: "Failed to add universal auth to already-configured identity" }); @@ -490,7 +489,7 @@ export const updateIdentityUniversalAuth = async (req: Request, res: Response) = return extractIPDetails(accessTokenTrustedIp.ipAddress); }); } - + const identityUniversalAuth = await IdentityUniversalAuth.findOneAndUpdate( { identity: identityMembershipOrg.identity._id, @@ -531,7 +530,7 @@ export const getIdentityUniversalAuth = async (req: Request, res: Response) => { const { params: { identityId } } = await validateRequest(reqValidator.GetUniversalAuthForIdentityV1, req); - + const identityMembershipOrg = await IdentityMembershipOrg .findOne({ identity: new Types.ObjectId(identityId) @@ -558,7 +557,7 @@ export const getIdentityUniversalAuth = async (req: Request, res: Response) => { if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.UNIVERSAL_AUTH) throw BadRequestError({ message: "The identity does not have universal auth configured" }); - + const identityUniversalAuth = await IdentityUniversalAuth.findOne({ identity: identityMembershipOrg.identity._id, }); @@ -625,11 +624,11 @@ export const createUniversalAuthClientSecret = async (req: Request, res: Respons const clientSecret = crypto.randomBytes(32).toString("hex"); const clientSecretHash = await bcrypt.hash(clientSecret, await getSaltRounds()); - + const identityUniversalAuth = await IdentityUniversalAuth.findOne({ identity: identityMembershipOrg.identity._id }); - + if (!identityUniversalAuth) throw ResourceNotFoundError(); const identityUniversalAuthClientSecret = await new IdentityUniversalAuthClientSecret({ @@ -665,7 +664,7 @@ export const getUniversalAuthClientSecrets = async (req: Request, res: Response) const { params: { identityId } } = await validateRequest(reqValidator.GetUniversalAuthClientSecretsV1, req); - + const identityMembershipOrg = await IdentityMembershipOrg.findOne({ identity: new Types.ObjectId(identityId) }).populate<{ @@ -725,7 +724,7 @@ export const revokeUniversalAuthClientSecret = async (req: Request, res: Respons const { params: { identityId, clientSecretId } } = await validateRequest(reqValidator.RevokeUniversalAuthClientSecretV1, req); - + const identityMembershipOrg = await IdentityMembershipOrg .findOne({ identity: new Types.ObjectId(identityId) @@ -773,7 +772,7 @@ export const revokeUniversalAuthClientSecret = async (req: Request, res: Respons ); if (!clientSecretData) throw ResourceNotFoundError(); - + await EEAuditLogService.createAuditLog( req.authData, { From c92c0f72889b3a0d4c4f77f6dc1b4503b5824b87 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 12 Dec 2023 19:36:48 -0500 Subject: [PATCH 2/3] add universal auth to agent --- cli/agent-config.yaml | 15 ++ cli/example-agent-config.yaml | 17 -- cli/packages/api/api.go | 36 +++- cli/packages/api/model.go | 27 ++- cli/packages/cmd/agent.go | 330 ++++++++++++++++++++++++++-------- 5 files changed, 315 insertions(+), 110 deletions(-) create mode 100644 cli/agent-config.yaml delete mode 100644 cli/example-agent-config.yaml diff --git a/cli/agent-config.yaml b/cli/agent-config.yaml new file mode 100644 index 000000000..ae130d3a8 --- /dev/null +++ b/cli/agent-config.yaml @@ -0,0 +1,15 @@ +infisical: + address: "http://localhost:8080" +auth: + type: "universal-auth" + config: + client-id: "./client-id" + client-secret: "./client-secret" + remove_client_secret_on_read: false +sinks: + - type: "file" + config: + path: "access-token" +templates: + - source-path: my-dot-ev-secret-template + destination-path: my-dot-env.env diff --git a/cli/example-agent-config.yaml b/cli/example-agent-config.yaml deleted file mode 100644 index 0afbe66f0..000000000 --- a/cli/example-agent-config.yaml +++ /dev/null @@ -1,17 +0,0 @@ -infisical: - address: "http://localhost:8080" -auth: - type: "token" - config: - token-path: "./role-id" -sinks: - - type: "file" - config: - path: "/Users/maidulislam/Desktop/test/infisical-token" - - type: "file" - config: - path: "access-token" - - type: "file" - config: - path: "maiduls-access-token" -templates: diff --git a/cli/packages/api/api.go b/cli/packages/api/api.go index c57cec59f..305df6e46 100644 --- a/cli/packages/api/api.go +++ b/cli/packages/api/api.go @@ -425,24 +425,44 @@ func CallCreateServiceToken(httpClient *resty.Client, request CreateServiceToken return createServiceTokenResponse, nil } -func CallServiceTokenV3Refresh(httpClient *resty.Client, request ServiceTokenV3RefreshTokenRequest) (ServiceTokenV3RefreshTokenResponse, error) { - var serviceTokenV3RefreshTokenResponse ServiceTokenV3RefreshTokenResponse +func CallUniversalAuthLogin(httpClient *resty.Client, request UniversalAuthLoginRequest) (UniversalAuthLoginResponse, error) { + var universalAuthLoginResponse UniversalAuthLoginResponse response, err := httpClient. R(). - SetResult(&serviceTokenV3RefreshTokenResponse). + SetResult(&universalAuthLoginResponse). SetHeader("User-Agent", USER_AGENT). SetBody(request). - Post(fmt.Sprintf("%v/v3/service-token/me/token", config.INFISICAL_URL)) + Post(fmt.Sprintf("%v/v1/auth/universal-auth/login/", config.INFISICAL_URL)) if err != nil { - return ServiceTokenV3RefreshTokenResponse{}, fmt.Errorf("CallServiceTokenV3Refresh: Unable to complete api request [err=%s]", err) + return UniversalAuthLoginResponse{}, fmt.Errorf("CallUniversalAuthLogin: Unable to complete api request [err=%s]", err) } if response.IsError() { - return ServiceTokenV3RefreshTokenResponse{}, fmt.Errorf("CallServiceTokenV3Refresh: Unsuccessful response [%v %v] [status-code=%v] [response=%v]", response.Request.Method, response.Request.URL, response.StatusCode(), response.String()) + return UniversalAuthLoginResponse{}, fmt.Errorf("CallUniversalAuthLogin: Unsuccessful response [%v %v] [status-code=%v] [response=%v]", response.Request.Method, response.Request.URL, response.StatusCode(), response.String()) } - return serviceTokenV3RefreshTokenResponse, nil + return universalAuthLoginResponse, nil +} + +func CallUniversalAuthRefreshAccessToken(httpClient *resty.Client, request UniversalAuthRefreshRequest) (UniversalAuthRefreshResponse, error) { + var universalAuthRefreshResponse UniversalAuthRefreshResponse + response, err := httpClient. + R(). + SetResult(&universalAuthRefreshResponse). + SetHeader("User-Agent", USER_AGENT). + SetBody(request). + Post(fmt.Sprintf("%v/v1/auth/token/renew", config.INFISICAL_URL)) + + if err != nil { + return UniversalAuthRefreshResponse{}, fmt.Errorf("CallUniversalAuthRefreshAccessToken: Unable to complete api request [err=%s]", err) + } + + if response.IsError() { + return UniversalAuthRefreshResponse{}, fmt.Errorf("CallUniversalAuthRefreshAccessToken: Unsuccessful response [%v %v] [status-code=%v] [response=%v]", response.Request.Method, response.Request.URL, response.StatusCode(), response.String()) + } + + return universalAuthRefreshResponse, nil } func CallGetRawSecretsV3(httpClient *resty.Client, request GetRawSecretsV3Request) (GetRawSecretsV3Response, error) { @@ -466,7 +486,7 @@ func CallGetRawSecretsV3(httpClient *resty.Client, request GetRawSecretsV3Reques } if response.IsError() { - return GetRawSecretsV3Response{}, fmt.Errorf("CallGetRawSecretsV3: Unsuccessful response [%v %v] [status-code=%v]", response.Request.Method, response.Request.URL, response.StatusCode()) + return GetRawSecretsV3Response{}, fmt.Errorf("CallUniversalAuthLogin: Unsuccessful response [%v %v] [status-code=%v] [response=%v]", response.Request.Method, response.Request.URL, response.StatusCode(), response.String()) } return getRawSecretsV3Response, nil diff --git a/cli/packages/api/model.go b/cli/packages/api/model.go index ceff88e71..3c6466382 100644 --- a/cli/packages/api/model.go +++ b/cli/packages/api/model.go @@ -463,14 +463,27 @@ type CreateServiceTokenResponse struct { ServiceTokenData ServiceTokenData `json:"serviceTokenData"` } -type ServiceTokenV3RefreshTokenRequest struct { - RefreshToken string `json:"refresh_token"` +type UniversalAuthLoginRequest struct { + ClientSecret string `json:"clientSecret"` + ClientId string `json:"clientId"` } -type ServiceTokenV3RefreshTokenResponse struct { - RefreshToken string `json:"refresh_token"` - AccessToken string `json:"access_token"` - ExpiresIn int `json:"expires_in"` - TokenType string `json:"token_type"` + +type UniversalAuthLoginResponse struct { + AccessToken string `json:"accessToken"` + AccessTokenTTL int `json:"expiresIn"` + TokenType string `json:"tokenType"` + AccessTokenMaxTTL int `json:"accessTokenMaxTTL"` +} + +type UniversalAuthRefreshRequest struct { + AccessToken string `json:"accessToken"` +} + +type UniversalAuthRefreshResponse struct { + AccessToken string `json:"accessToken"` + AccessTokenTTL int `json:"expiresIn"` + TokenType string `json:"tokenType"` + AccessTokenMaxTTL int `json:"accessTokenMaxTTL"` } type GetRawSecretsV3Request struct { diff --git a/cli/packages/cmd/agent.go b/cli/packages/cmd/agent.go index 83986021d..21f9613f7 100644 --- a/cli/packages/cmd/agent.go +++ b/cli/packages/cmd/agent.go @@ -5,12 +5,12 @@ package cmd import ( "bytes" - "errors" "fmt" "io/ioutil" "os" "os/signal" "strings" + "sync" "syscall" "text/template" "time" @@ -44,8 +44,10 @@ type AuthConfig struct { Config interface{} `yaml:"config"` } -type TokenAuthConfig struct { - TokenPath string `yaml:"token-path"` +type UniversalAuth struct { + ClientIDPath string `yaml:"client-id"` + ClientSecretPath string `yaml:"client-secret"` + RemoveClientSecretOnRead bool `yaml:"remove_client_secret_on_read"` } type OAuthConfig struct { @@ -149,11 +151,12 @@ func ParseAgentConfig(filePath string) (*Config, error) { } switch rawConfig.Auth.Type { - case "token": - var tokenConfig TokenAuthConfig + case "universal-auth": + var tokenConfig UniversalAuth if err := yaml.Unmarshal(configBytes, &tokenConfig); err != nil { return nil, err } + config.Auth.Config = tokenConfig case "oauth": // aws, gcp, k8s service account, etc var oauthConfig OAuthConfig @@ -199,59 +202,235 @@ func ProcessTemplate(templatePath string, data interface{}, accessToken string) return &buf, nil } -func refreshTokenAndProcessTemplate(refreshToken string, config *Config, errChan chan error) { - for { - httpClient := resty.New() - httpClient.SetRetryCount(10000). - SetRetryMaxWaitTime(20 * time.Second). - SetRetryWaitTime(5 * time.Second) +type TokenManager struct { + accessToken string + accessTokenTTL time.Duration + accessTokenMaxTTL time.Duration + accessTokenFetchedTime time.Time + accessTokenRefreshedTime time.Time + mutex sync.Mutex + filePaths []Sink // Store file paths if needed + templates []Template + clientIdPath string + clientSecretPath string + newAccessTokenNotificationChan chan bool + removeClientSecretOnRead bool + cachedClientSecret string +} - tokenResponse, err := api.CallServiceTokenV3Refresh(httpClient, api.ServiceTokenV3RefreshTokenRequest{RefreshToken: refreshToken}) - if err != nil { - errChan <- fmt.Errorf("unable to complete renewal because [%s]", err) - } +func NewTokenManager(fileDeposits []Sink, templates []Template, clientIdPath string, clientSecretPath string, newAccessTokenNotificationChan chan bool, removeClientSecretOnRead bool) *TokenManager { + return &TokenManager{filePaths: fileDeposits, templates: templates, clientIdPath: clientIdPath, clientSecretPath: clientSecretPath, newAccessTokenNotificationChan: newAccessTokenNotificationChan, removeClientSecretOnRead: removeClientSecretOnRead} +} - for _, sinkFile := range config.Sinks { - if sinkFile.Type == "file" { - err = ioutil.WriteFile(sinkFile.Config.Path, []byte(tokenResponse.AccessToken), 0644) - if err != nil { - errChan <- err - return - } - } else { - errChan <- errors.New("unsupported sink type. Only 'file' type is supported") - return - } - } +func (tm *TokenManager) SetToken(token string, accessTokenTTL time.Duration, accessTokenMaxTTL time.Duration) { + tm.mutex.Lock() + defer tm.mutex.Unlock() - refreshToken = tokenResponse.RefreshToken - nextRefreshCycle := time.Duration(tokenResponse.ExpiresIn-5) * time.Second // when the next access refresh will happen + tm.accessToken = token + tm.accessTokenTTL = accessTokenTTL + tm.accessTokenMaxTTL = accessTokenMaxTTL - d, err := time.ParseDuration(nextRefreshCycle.String()) - if err != nil { - errChan <- fmt.Errorf("unable to parse refresh time because %s", err) - return - } + tm.newAccessTokenNotificationChan <- true +} - log.Info().Msgf("token refreshed and saved to selected path; next cycle will occur in %s", d.String()) +func (tm *TokenManager) GetToken() string { + tm.mutex.Lock() + defer tm.mutex.Unlock() - for _, secretTemplate := range config.Templates { - processedTemplate, err := ProcessTemplate(secretTemplate.SourcePath, nil, tokenResponse.AccessToken) - if err != nil { - errChan <- err - return - } + return tm.accessToken +} - if err := WriteBytesToFile(processedTemplate, secretTemplate.DestinationPath); err != nil { - errChan <- err - return - } - - log.Info().Msgf("secret template at path %s has been rendered and saved to path %s", secretTemplate.SourcePath, secretTemplate.DestinationPath) - } - - time.Sleep(nextRefreshCycle) +// Fetches a new access token using client credentials +func (tm *TokenManager) FetchNewAccessToken() error { + clientIDAsByte, err := ReadFile(tm.clientIdPath) + if err != nil { + return fmt.Errorf("unable to read client id from file path '%s' due to error: %v", tm.clientIdPath, err) } + + clientSecretAsByte, err := ReadFile(tm.clientSecretPath) + if err != nil { + if len(tm.cachedClientSecret) == 0 { + return fmt.Errorf("unable to read client secret from file and no cached client secret found: %v", err) + } else { + clientSecretAsByte = []byte(tm.cachedClientSecret) + } + } + + // remove client secret after first read + if tm.removeClientSecretOnRead { + os.Remove(tm.clientSecretPath) + } + + clientId := string(clientIDAsByte) + clientSecret := string(clientSecretAsByte) + + // save as cache in memory + tm.cachedClientSecret = clientSecret + + err, loginResponse := universalAuthLogin(clientId, clientSecret) + if err != nil { + return err + } + + accessTokenTTL := time.Duration(loginResponse.AccessTokenTTL * int(time.Second)) + accessTokenMaxTTL := time.Duration(loginResponse.AccessTokenMaxTTL * int(time.Second)) + + if accessTokenTTL <= time.Duration(5)*time.Second { + util.PrintErrorMessageAndExit("At this this, agent does not support refresh of tokens with 5 seconds or less ttl. Please increase access token ttl and try again") + } + + tm.accessTokenFetchedTime = time.Now() + tm.SetToken(loginResponse.AccessToken, accessTokenTTL, accessTokenMaxTTL) + + return nil +} + +// Refreshes the existing access token +func (tm *TokenManager) RefreshAccessToken() error { + httpClient := resty.New() + httpClient.SetRetryCount(10000). + SetRetryMaxWaitTime(20 * time.Second). + SetRetryWaitTime(5 * time.Second) + + accessToken := tm.GetToken() + response, err := api.CallUniversalAuthRefreshAccessToken(httpClient, api.UniversalAuthRefreshRequest{AccessToken: accessToken}) + if err != nil { + return err + } + + accessTokenTTL := time.Duration(response.AccessTokenTTL * int(time.Second)) + accessTokenMaxTTL := time.Duration(response.AccessTokenMaxTTL * int(time.Second)) + tm.accessTokenRefreshedTime = time.Now() + + tm.SetToken(response.AccessToken, accessTokenTTL, accessTokenMaxTTL) + + return nil +} + +func (tm *TokenManager) ManageTokenLifecycle() { + for { + accessTokenMaxTTLExpiresInTime := tm.accessTokenFetchedTime.Add(tm.accessTokenMaxTTL - (5 * time.Second)) + accessTokenRefreshedTime := tm.accessTokenRefreshedTime + + if accessTokenRefreshedTime.IsZero() { + accessTokenRefreshedTime = tm.accessTokenFetchedTime + } + + nextAccessTokenExpiresInTime := accessTokenRefreshedTime.Add(tm.accessTokenTTL - (5 * time.Second)) + + if tm.accessTokenFetchedTime.IsZero() && tm.accessTokenRefreshedTime.IsZero() { + // case: init login to get access token + log.Info().Msg("attempting to authenticate...") + err := tm.FetchNewAccessToken() + if err != nil { + log.Error().Msgf("unable to authenticate because %v. Will retry in 30 seconds", err) + + // wait a bit before trying again + time.Sleep((30 * time.Second)) + continue + } + } else if time.Now().After(accessTokenMaxTTLExpiresInTime) { + log.Info().Msgf("token has reached max ttl, attempting to re authenticate...") + err := tm.FetchNewAccessToken() + if err != nil { + log.Error().Msgf("unable to authenticate because %v. Will retry in 30 seconds", err) + + // wait a bit before trying again + time.Sleep((30 * time.Second)) + continue + } + } else { + log.Info().Msgf("attempting to refresh existing token...") + err := tm.RefreshAccessToken() + if err != nil { + log.Error().Msgf("unable to refresh token because %v. Will retry in 30 seconds", err) + + // wait a bit before trying again + time.Sleep((30 * time.Second)) + continue + } + } + + if accessTokenRefreshedTime.IsZero() { + accessTokenRefreshedTime = tm.accessTokenFetchedTime + } else { + accessTokenRefreshedTime = tm.accessTokenRefreshedTime + } + + nextAccessTokenExpiresInTime = accessTokenRefreshedTime.Add(tm.accessTokenTTL - (5 * time.Second)) + accessTokenMaxTTLExpiresInTime = tm.accessTokenFetchedTime.Add(tm.accessTokenMaxTTL - (5 * time.Second)) + + if nextAccessTokenExpiresInTime.After(accessTokenMaxTTLExpiresInTime) { + // case: Refreshed so close that the next refresh would occur beyond max ttl (this is because currently, token renew tries to add +access-token-ttl amount of time) + // example: access token ttl is 11 sec and max ttl is 30 sec. So it will start with 11 seconds, then 22 seconds but the next time you call refresh it would try to extend it to 33 but max ttl only allows 30, so the token will be valid until 30 before we need to reauth + time.Sleep(tm.accessTokenTTL - nextAccessTokenExpiresInTime.Sub(accessTokenMaxTTLExpiresInTime)) + } else { + time.Sleep(tm.accessTokenTTL - (5 * time.Second)) + } + } +} + +func (tm *TokenManager) WriteTokenToFiles() { + token := tm.GetToken() + for _, sinkFile := range tm.filePaths { + if sinkFile.Type == "file" { + err := ioutil.WriteFile(sinkFile.Config.Path, []byte(token), 0644) + if err != nil { + log.Error().Msgf("unable to write file sink to path '%s' because %v", sinkFile.Config.Path, err) + } + + log.Info().Msgf("new access token saved to file at path '%s'", sinkFile.Config.Path) + + } else { + log.Error().Msg("unsupported sink type. Only 'file' type is supported") + } + } +} + +func (tm *TokenManager) FetchSecrets() { + log.Info().Msgf("template engine started...") + for { + token := tm.GetToken() + if token != "" { + for _, secretTemplate := range tm.templates { + processedTemplate, err := ProcessTemplate(secretTemplate.SourcePath, nil, token) + if err != nil { + log.Error().Msgf("template engine: unable to render secrets because %s. Will try again in 30 seconds", err) + + // wait a bit before trying again + time.Sleep((30 * time.Second)) + continue + } + + if err := WriteBytesToFile(processedTemplate, secretTemplate.DestinationPath); err != nil { + log.Error().Msgf("template engine: unable to write secrets to path because %s. Will try again in 30 seconds", err) + + // wait a bit before trying again + time.Sleep((30 * time.Second)) + continue + } + + log.Info().Msgf("template engine: secret template at path %s has been rendered and saved to path %s", secretTemplate.SourcePath, secretTemplate.DestinationPath) + } + + // fetch new secrets every 5 minutes (TODO: add PubSub in the future ) + time.Sleep(5 * time.Minute) + } + } +} + +func universalAuthLogin(clientId string, clientSecret string) (error, api.UniversalAuthLoginResponse) { + httpClient := resty.New() + httpClient.SetRetryCount(10000). + SetRetryMaxWaitTime(20 * time.Second). + SetRetryWaitTime(5 * time.Second) + + tokenResponse, err := api.CallUniversalAuthLogin(httpClient, api.UniversalAuthLoginRequest{ClientId: clientId, ClientSecret: clientSecret}) + if err != nil { + return err, api.UniversalAuthLoginResponse{} + } + + return nil, tokenResponse } // runCmd represents the run command @@ -282,36 +461,31 @@ var agentCmd = &cobra.Command{ return } - errChan := make(chan error) - sigChan := make(chan os.Signal, 1) - - signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) - - switch configAuthType := agentConfig.Auth.Config.(type) { - case TokenAuthConfig: - content, err := ReadFile(configAuthType.TokenPath) - if err != nil { - log.Error().Msgf("unable to read initial token from file path %s because %v", configAuthType.TokenPath, err) - return - } - - refreshToken := string(content) - go refreshTokenAndProcessTemplate(refreshToken, agentConfig, errChan) - - case OAuthConfig: - // future auth types - default: - log.Error().Msgf("unknown auth config type. Only 'file' type is supported") - return + if agentConfig.Auth.Type != "universal-auth" { + util.PrintErrorMessageAndExit("Only auth type of 'universal-auth' is supported at this time") } - select { - case err := <-errChan: - log.Fatal().Msgf("agent stopped due to error: %v", err) - os.Exit(1) - case <-sigChan: - log.Info().Msg("agent is gracefully shutting...") - os.Exit(1) + configUniversalAuthType := agentConfig.Auth.Config.(UniversalAuth) + + tokenRefreshNotifier := make(chan bool) + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + + filePaths := agentConfig.Sinks + tm := NewTokenManager(filePaths, agentConfig.Templates, configUniversalAuthType.ClientIDPath, configUniversalAuthType.ClientSecretPath, tokenRefreshNotifier, configUniversalAuthType.RemoveClientSecretOnRead) + + go tm.ManageTokenLifecycle() + go tm.FetchSecrets() + + for { + select { + case <-tokenRefreshNotifier: + go tm.WriteTokenToFiles() + case <-sigChan: + log.Info().Msg("agent is gracefully shutting...") + // TODO: check if we are in the middle of writing files to disk + os.Exit(1) + } } }, From cc4ad1df4b4091a52184dd93aec539efa1b7046f Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 12 Dec 2023 20:24:17 -0500 Subject: [PATCH 3/3] update docs for agent --- docs/infisical-agent/overview.mdx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/infisical-agent/overview.mdx b/docs/infisical-agent/overview.mdx index 695d95829..c197217ad 100644 --- a/docs/infisical-agent/overview.mdx +++ b/docs/infisical-agent/overview.mdx @@ -12,7 +12,7 @@ It eliminates the need to modify application logic by enabling clients to decide - Templating: Renders secrets via user provided templates to desired formats for applications to consume ### Token renewal -The Infisical agent can help manage the life cycle of access tokens. The token renewal process is split into two main components: a Method, which is the authentication process suitable for your current setup, and Sinks, which are the places where the agent deposits the new access token whenever it receives updates. +The Infisical agent can help manage the life cycle of access tokens. The token renewal process is split into two main components: a `Method`, which is the authentication process suitable for your current setup, and `Sinks`, which are the places where the agent deposits the new access token whenever it receives updates. When the Infisical Agent is started, it will attempt to obtain a valid access token using the authentication method you have configured. If the agent is unable to fetch a valid token, the agent will keep trying, increasing the time between each attempt. @@ -43,8 +43,10 @@ While specifying an authentication method is mandatory to start the agent, confi | Field | Description | | ---------------------------- | ----------- | | `infisical.address` | The URL of the Infisical service. Default: `"https://app.infisical.com"`. | -| `auth.type` | The type of authentication method used. Only `"token"` type is currently available | -| `auth.config.token-path` | The file path where the initial token for authentication is stored. | +| `auth.type` | The type of authentication method used. Only `"universal-auth"` type is currently available | +| `auth.config.client-id` | The file path where the universal-auth client id is stored. | +| `auth.config.client-secret` | The file path where the universal-auth client secret is stored. | +| `auth.config.remove_client_secret_on_read` | This will instruct the agent to remove the client secret from disk. | | `sinks[].type` | The type of sink in a list of sinks. Each item specifies a sink type. Currently, only `"file"` type is available. | | `sinks[].config.path` | The file path where the access token should be stored for each sink in the list. | | `templates[].source-path` | The path to the template file that should be used to render secrets. | @@ -60,9 +62,11 @@ Once you have the CLI installed, you will need to create a agent configuration f infisical: address: "https://app.infisical.com" auth: - type: "token" + type: "universal-auth" config: - token-path: "/path/to/initial/token" + client-id: "./client-id" + client-secret: "./client-secret" + remove_client_secret_on_read: false sinks: - type: "file" config: