diff --git a/controller/internal/routes/authenticate_router.go b/controller/internal/routes/authenticate_router.go index e50ed0fad..d482a5709 100644 --- a/controller/internal/routes/authenticate_router.go +++ b/controller/internal/routes/authenticate_router.go @@ -126,28 +126,30 @@ func (ro *AuthRouter) authHandler(ae *env.AppEnv, rc *response.RequestContext, p ConfigTypes: configTypes, IPAddress: remoteIpStr, } - sessionId, err := ae.Handlers.ApiSession.Create(s) + apiSessionId, err := ae.Handlers.ApiSession.Create(s) if err != nil { rc.RespondWithError(err) return } - session, err := ae.Handlers.ApiSession.Read(sessionId) + updatedApiSesion, err := ae.Handlers.ApiSession.Read(apiSessionId) if err != nil { logger.WithField("cause", err).Error("loading session by id resulted in an error") rc.RespondWithApiError(apierror.NewUnauthorized()) } - apiSession := MapToCurrentApiSessionRestModel(session, ae.Config.SessionTimeoutDuration()) + rc.ApiSession = updatedApiSesion + + apiSession := MapToCurrentApiSessionRestModel(updatedApiSesion, ae.Config.SessionTimeoutDuration()) envelope := &rest_model.CurrentAPISessionDetailEnvelope{Data: apiSession, Meta: &rest_model.Meta{}} expiration := time.Time(*apiSession.ExpiresAt) cookie := http.Cookie{Name: ae.AuthCookieName, Value: token, Expires: expiration} - rc.ResponseWriter.Header().Set(ae.AuthHeaderName, session.Token) + rc.ResponseWriter.Header().Set(ae.AuthHeaderName, updatedApiSesion.Token) http.SetCookie(rc.ResponseWriter, &cookie) ro.createTimer.UpdateSince(start) diff --git a/controller/internal/routes/ca_router.go b/controller/internal/routes/ca_router.go index 9a1cb1afc..3e6eb611b 100644 --- a/controller/internal/routes/ca_router.go +++ b/controller/internal/routes/ca_router.go @@ -285,6 +285,7 @@ func (r *CaRouter) generateJwt(ae *env.AppEnv, rc *response.RequestContext) { rc.ResponseWriter.Header().Set("content-type", "application/jwt") response.AddVersionHeader(rc.ResponseWriter) + response.AddSessionExpirationHeader(rc) rc.ResponseWriter.WriteHeader(http.StatusOK) _, _ = rc.ResponseWriter.Write([]byte(jwt)) } diff --git a/controller/internal/routes/current_api_session_api_model.go b/controller/internal/routes/current_api_session_api_model.go index 83d36e4d4..1b44af5d5 100644 --- a/controller/internal/routes/current_api_session_api_model.go +++ b/controller/internal/routes/current_api_session_api_model.go @@ -59,6 +59,7 @@ func (factory *CurrentApiSessionLinkFactoryImpl) Links(entity models.Entity) res func MapToCurrentApiSessionRestModel(s *model.ApiSession, sessionTimeout time.Duration) *rest_model.CurrentAPISessionDetail { expiresAt := strfmt.DateTime(s.UpdatedAt.Add(sessionTimeout)) + expirationSeconds := int64(s.ExpirationDuration.Seconds()) apiSession := &rest_model.CurrentAPISessionDetail{ APISessionDetail: rest_model.APISessionDetail{ BaseEntity: BaseEntityToRestModel(s, CurrentApiSessionLinkFactory), @@ -67,7 +68,8 @@ func MapToCurrentApiSessionRestModel(s *model.ApiSession, sessionTimeout time.Du ConfigTypes: stringz.SetToSlice(s.ConfigTypes), IPAddress: &s.IPAddress, }, - ExpiresAt: &expiresAt, + ExpiresAt: &expiresAt, + ExpirationSeconds: &expirationSeconds, } return apiSession diff --git a/controller/model/api_session_model.go b/controller/model/api_session_model.go index 1f7dc3b13..7cac3e02d 100644 --- a/controller/model/api_session_model.go +++ b/controller/model/api_session_model.go @@ -25,15 +25,18 @@ import ( "github.com/pkg/errors" "go.etcd.io/bbolt" "reflect" + "time" ) type ApiSession struct { models.BaseEntity - Token string - IdentityId string - Identity *Identity - IPAddress string - ConfigTypes map[string]struct{} + Token string + IdentityId string + Identity *Identity + IPAddress string + ConfigTypes map[string]struct{} + ExpiresAt time.Time + ExpirationDuration time.Duration } func (entity *ApiSession) toBoltEntity(tx *bbolt.Tx, handler Handler) (boltz.Entity, error) { @@ -74,6 +77,9 @@ func (entity *ApiSession) fillFrom(handler Handler, tx *bbolt.Tx, boltEntity bol entity.IdentityId = boltApiSession.IdentityId entity.ConfigTypes = stringz.SliceToSet(boltApiSession.ConfigTypes) entity.IPAddress = boltApiSession.IPAddress + entity.ExpiresAt = entity.UpdatedAt.Add(handler.GetEnv().GetConfig().Api.SessionTimeoutSeconds) + entity.ExpirationDuration = handler.GetEnv().GetConfig().Api.SessionTimeoutSeconds + boltIdentity, err := handler.GetEnv().GetStores().Identity.LoadOneById(tx, boltApiSession.IdentityId) if err != nil { return err diff --git a/controller/response/headers.go b/controller/response/headers.go index 31bae346c..d1fa595ef 100644 --- a/controller/response/headers.go +++ b/controller/response/headers.go @@ -21,9 +21,22 @@ import ( "github.com/openziti/edge/build" version3 "github.com/openziti/edge/internal/version" "net/http" + "strconv" +) + +const ( + ApiSessionExpirationSecondsHeader = "expiration-seconds" + ApiSessionExpiresAtheader = "expires-at" ) func AddVersionHeader(rw http.ResponseWriter) { buildInfo := build.GetBuildInfo() rw.Header().Set(ZitiControllerVersionHeader, fmt.Sprintf("%s/%s/%s", buildInfo.Version(), buildInfo.Revision(), version3.GetApiVersion())) } + +func AddSessionExpirationHeader(rc *RequestContext) { + if rc.ApiSession != nil { + rc.ResponseWriter.Header().Set(ApiSessionExpirationSecondsHeader, strconv.FormatInt(int64(rc.ApiSession.ExpirationDuration.Seconds()), 10)) + rc.ResponseWriter.Header().Set(ApiSessionExpiresAtheader, rc.ApiSession.ExpiresAt.String()) + } +} diff --git a/controller/response/responder.go b/controller/response/responder.go index 34921ba44..ebf37b176 100644 --- a/controller/response/responder.go +++ b/controller/response/responder.go @@ -115,14 +115,17 @@ func (responder *ResponderImpl) RespondWithOk(data interface{}, meta *rest_model } func (responder *ResponderImpl) Respond(data interface{}, httpStatus int) { + AddSessionExpirationHeader(responder.rc) Respond(responder.rc.ResponseWriter, responder.rc.Request.URL.Path, responder.rc.Id, responder.GetProducer(), data, httpStatus) } func (responder *ResponderImpl) RespondWithError(err error) { + AddSessionExpirationHeader(responder.rc) RespondWithError(responder.rc.ResponseWriter, responder.rc.Request, responder.rc.Id, responder.GetProducer(), err) } func (responder *ResponderImpl) RespondWithApiError(apiError *apierror.ApiError) { + AddSessionExpirationHeader(responder.rc) RespondWithApiError(responder.rc.ResponseWriter, responder.rc.Request, responder.rc.Id, responder.GetProducer(), apiError) } diff --git a/rest_model/current_api_session_detail.go b/rest_model/current_api_session_detail.go index 0ea663174..e4b3987d4 100644 --- a/rest_model/current_api_session_detail.go +++ b/rest_model/current_api_session_detail.go @@ -42,6 +42,10 @@ import ( type CurrentAPISessionDetail struct { APISessionDetail + // expiration seconds + // Required: true + ExpirationSeconds *int64 `json:"expirationSeconds"` + // expires at // Required: true // Format: date-time @@ -59,12 +63,16 @@ func (m *CurrentAPISessionDetail) UnmarshalJSON(raw []byte) error { // AO1 var dataAO1 struct { + ExpirationSeconds *int64 `json:"expirationSeconds"` + ExpiresAt *strfmt.DateTime `json:"expiresAt"` } if err := swag.ReadJSON(raw, &dataAO1); err != nil { return err } + m.ExpirationSeconds = dataAO1.ExpirationSeconds + m.ExpiresAt = dataAO1.ExpiresAt return nil @@ -80,9 +88,13 @@ func (m CurrentAPISessionDetail) MarshalJSON() ([]byte, error) { } _parts = append(_parts, aO0) var dataAO1 struct { + ExpirationSeconds *int64 `json:"expirationSeconds"` + ExpiresAt *strfmt.DateTime `json:"expiresAt"` } + dataAO1.ExpirationSeconds = m.ExpirationSeconds + dataAO1.ExpiresAt = m.ExpiresAt jsonDataAO1, errAO1 := swag.WriteJSON(dataAO1) @@ -102,6 +114,10 @@ func (m *CurrentAPISessionDetail) Validate(formats strfmt.Registry) error { res = append(res, err) } + if err := m.validateExpirationSeconds(formats); err != nil { + res = append(res, err) + } + if err := m.validateExpiresAt(formats); err != nil { res = append(res, err) } @@ -112,6 +128,15 @@ func (m *CurrentAPISessionDetail) Validate(formats strfmt.Registry) error { return nil } +func (m *CurrentAPISessionDetail) validateExpirationSeconds(formats strfmt.Registry) error { + + if err := validate.Required("expirationSeconds", "body", m.ExpirationSeconds); err != nil { + return err + } + + return nil +} + func (m *CurrentAPISessionDetail) validateExpiresAt(formats strfmt.Registry) error { if err := validate.Required("expiresAt", "body", m.ExpiresAt); err != nil { diff --git a/rest_model/posture_check_list.go b/rest_model/posture_check_list.go index 378ab8e20..8bbe9196b 100644 --- a/rest_model/posture_check_list.go +++ b/rest_model/posture_check_list.go @@ -99,4 +99,4 @@ func unmarshalPostureCheckList(data []byte, consumer runtime.Consumer) (PostureC switch getType.Empty { } return nil, errors.New(422, "invalid value: %q", getType.Empty) -} \ No newline at end of file +} diff --git a/rest_server/embedded_spec.go b/rest_server/embedded_spec.go index 159503b17..7ff5a5238 100644 --- a/rest_server/embedded_spec.go +++ b/rest_server/embedded_spec.go @@ -6583,9 +6583,13 @@ func init() { { "type": "object", "required": [ - "expiresAt" + "expiresAt", + "expirationSeconds" ], "properties": { + "expirationSeconds": { + "type": "integer" + }, "expiresAt": { "type": "string", "format": "date-time" @@ -24882,9 +24886,13 @@ func init() { { "type": "object", "required": [ - "expiresAt" + "expiresAt", + "expirationSeconds" ], "properties": { + "expirationSeconds": { + "type": "integer" + }, "expiresAt": { "type": "string", "format": "date-time" diff --git a/specs/swagger.yml b/specs/swagger.yml index 197aa302f..7ea952c1a 100644 --- a/specs/swagger.yml +++ b/specs/swagger.yml @@ -4136,10 +4136,13 @@ definitions: - type: object required: - expiresAt + - expirationSeconds properties: expiresAt: type: string format: date-time + expirationSeconds: + type: integer ################################################################### # Identities ##################################################################