diff --git a/driver/config/config.go b/driver/config/config.go index b0debaa9b48a..4ac01cf46c18 100644 --- a/driver/config/config.go +++ b/driver/config/config.go @@ -95,6 +95,8 @@ const ( ViperKeySessionPath = "session.cookie.path" ViperKeySessionPersistentCookie = "session.cookie.persistent" ViperKeySessionWhoAmIAAL = "session.whoami.required_aal" + ViperKeySessionWhoAmIRefreshAllowed = "session.whoami.refresh_allowed" + ViperKeySessionRefreshMinTimeLeft = "session.refresh_min_time_left" ViperKeyCookieSameSite = "cookies.same_site" ViperKeyCookieDomain = "cookies.domain" ViperKeyCookiePath = "cookies.path" @@ -1029,6 +1031,14 @@ func (p *Config) SessionWhoAmIAAL() string { return p.p.String(ViperKeySessionWhoAmIAAL) } +func (p *Config) SessionWhoAmIRefreshAllowed() bool { + return p.p.Bool(ViperKeySessionWhoAmIRefreshAllowed) +} + +func (p *Config) SessionRefreshMinTimeLeft() time.Duration { + return p.p.DurationF(ViperKeySessionRefreshMinTimeLeft, p.SessionLifespan()) +} + func (p *Config) SelfServiceSettingsRequiredAAL() string { return p.p.String(ViperKeySelfServiceSettingsRequiredAAL) } diff --git a/driver/config/config_test.go b/driver/config/config_test.go index 332a2a26534e..094f1490e6e4 100644 --- a/driver/config/config_test.go +++ b/driver/config/config_test.go @@ -598,6 +598,10 @@ func TestSession(t *testing.T) { p.MustSet(config.ViperKeySessionName, "ory_session") assert.Equal(t, "ory_session", p.SessionName()) + assert.Equal(t, time.Hour*24, p.SessionRefreshMinTimeLeft()) + p.MustSet(config.ViperKeySessionRefreshMinTimeLeft, "1m") + assert.Equal(t, time.Minute, p.SessionRefreshMinTimeLeft()) + assert.Equal(t, time.Hour*24, p.SessionLifespan()) p.MustSet(config.ViperKeySessionLifespan, "1m") assert.Equal(t, time.Minute, p.SessionLifespan()) @@ -605,6 +609,10 @@ func TestSession(t *testing.T) { assert.Equal(t, true, p.SessionPersistentCookie()) p.MustSet(config.ViperKeySessionPersistentCookie, false) assert.Equal(t, false, p.SessionPersistentCookie()) + + assert.Equal(t, false, p.SessionWhoAmIRefreshAllowed()) + p.MustSet(config.ViperKeySessionWhoAmIRefreshAllowed, true) + assert.Equal(t, true, p.SessionWhoAmIRefreshAllowed()) } func TestCookies(t *testing.T) { diff --git a/embedx/config.schema.json b/embedx/config.schema.json index 8347853bb213..a45c289e6cb5 100644 --- a/embedx/config.schema.json +++ b/embedx/config.schema.json @@ -2115,6 +2115,12 @@ "properties": { "required_aal": { "$ref": "#/definitions/featureRequiredAal" + }, + "refresh_allowed": { + "title": "Allow session refresh from whoami endpoint", + "description": "If set to true will allow to refresh session lifespan if ?refresh=true is present", + "type": "boolean", + "default": false } }, "additionalProperties": false @@ -2168,6 +2174,18 @@ } }, "additionalProperties": false + }, + "refresh_min_time_left": { + "title": "Session refresh minimal time left", + "description": "Time window when session can be refreshed to avoid excess refreshes. It is calculated as duration from the session expiration time.", + "type": "string", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "default": "24h", + "examples": [ + "1h", + "1m", + "1s" + ] } } }, diff --git a/internal/httpclient/.openapi-generator/FILES b/internal/httpclient/.openapi-generator/FILES index 73915f3caef4..93abe7af405b 100644 --- a/internal/httpclient/.openapi-generator/FILES +++ b/internal/httpclient/.openapi-generator/FILES @@ -3,6 +3,7 @@ .travis.yml README.md api/openapi.yaml +api_default.go api_metadata.go api_v0alpha2.go client.go @@ -11,6 +12,7 @@ docs/AdminCreateIdentityBody.md docs/AdminCreateSelfServiceRecoveryLinkBody.md docs/AdminUpdateIdentityBody.md docs/AuthenticatorAssuranceLevel.md +docs/DefaultApi.md docs/ErrorAuthenticatorAssuranceLevelNotSatisfied.md docs/GenericError.md docs/HealthNotReadyStatus.md diff --git a/internal/httpclient/README.md b/internal/httpclient/README.md index dddbc40fead8..fc898fd0ba0e 100644 --- a/internal/httpclient/README.md +++ b/internal/httpclient/README.md @@ -83,6 +83,7 @@ All URIs are relative to *http://localhost* Class | Method | HTTP request | Description ------------ | ------------- | ------------- | ------------- +*DefaultApi* | [**V0alpha2**](docs/DefaultApi.md#v0alpha2) | **Get** /sessions/refresh | Calling this endpoint refreshes a current user session. If `session.refresh_time_window` is set it will only refresh the session after this time has passed. *MetadataApi* | [**GetVersion**](docs/MetadataApi.md#getversion) | **Get** /version | Return Running Software Version. *MetadataApi* | [**IsAlive**](docs/MetadataApi.md#isalive) | **Get** /health/alive | Check HTTP Server Status *MetadataApi* | [**IsReady**](docs/MetadataApi.md#isready) | **Get** /health/ready | Check HTTP Server and Database Status @@ -93,6 +94,7 @@ Class | Method | HTTP request | Description *V0alpha2Api* | [**AdminGetIdentity**](docs/V0alpha2Api.md#admingetidentity) | **Get** /identities/{id} | Get an Identity *V0alpha2Api* | [**AdminIdentitySession**](docs/V0alpha2Api.md#adminidentitysession) | **Get** /identities/{id}/session | Calling this endpoint issues a session for a given identity. *V0alpha2Api* | [**AdminListIdentities**](docs/V0alpha2Api.md#adminlistidentities) | **Get** /identities | List Identities +*V0alpha2Api* | [**AdminSessionRefresh**](docs/V0alpha2Api.md#adminsessionrefresh) | **Patch** /sessions/refresh/{id} | Calling this endpoint refreshes a given session. If `session.refresh_min_time_left` is set it will only refresh the session after this time has passed. *V0alpha2Api* | [**AdminUpdateCredentials**](docs/V0alpha2Api.md#adminupdatecredentials) | **Put** /identities/{id}/credentials | Update Identity Credentials *V0alpha2Api* | [**AdminUpdateIdentity**](docs/V0alpha2Api.md#adminupdateidentity) | **Put** /identities/{id} | Update an Identity *V0alpha2Api* | [**CreateSelfServiceLogoutFlowUrlForBrowsers**](docs/V0alpha2Api.md#createselfservicelogoutflowurlforbrowsers) | **Get** /self-service/logout/browser | Create a Logout URL for Browsers diff --git a/internal/httpclient/api/openapi.yaml b/internal/httpclient/api/openapi.yaml index e06d1e36601d..ea0f908aac3d 100644 --- a/internal/httpclient/api/openapi.yaml +++ b/internal/httpclient/api/openapi.yaml @@ -2227,6 +2227,79 @@ paths: summary: Get Verification Flow tags: - v0alpha2 + /sessions/refresh: + get: + description: |- + This endpoint is useful for: + + Session refresh + operationId: v0alpha2 + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/successfulAdminIdentitySession' + description: successfulAdminIdentitySession + "404": + content: + application/json: + schema: + $ref: '#/components/schemas/jsonError' + description: jsonError + "500": + content: + application/json: + schema: + $ref: '#/components/schemas/jsonError' + description: jsonError + security: + - oryAccessToken: [] + summary: |- + Calling this endpoint refreshes a current user session. + If `session.refresh_time_window` is set it will only refresh the session after this time has passed. + /sessions/refresh/{id}: + patch: + description: |- + This endpoint is useful for: + + Session refresh + operationId: adminSessionRefresh + parameters: + - description: ID is the session's ID. + explode: false + in: path + name: id + required: true + schema: + type: string + style: simple + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/session' + description: session + "404": + content: + application/json: + schema: + $ref: '#/components/schemas/jsonError' + description: jsonError + "500": + content: + application/json: + schema: + $ref: '#/components/schemas/jsonError' + description: jsonError + security: + - oryAccessToken: [] + summary: |- + Calling this endpoint refreshes a given session. + If `session.refresh_min_time_left` is set it will only refresh the session after this time has passed. + tags: + - v0alpha2 /sessions/whoami: get: description: |- @@ -2234,6 +2307,12 @@ paths: Returns a session object in the body or 401 if the credentials are invalid or no credentials were sent. Additionally when the request it successful it adds the user ID to the 'X-Kratos-Authenticated-Identity-Id' header in the response. + It is also possible to refresh the session lifespan of a current session by adding a `refresh=true` param to the request url. + By default session refresh on this endpoint is disabled. + Session refresh can be enabled only after setting `session.whoami.refresh_allowed` to true in the config. + After enabling this option any refresh request will set the session life equal to `session.lifespan`. + If you want to refresh the session only some time before session expiration you can set a proper value for `session.refresh_min_time_left` + If you call this endpoint from a server-side application, you must forward the HTTP Cookie Header to this endpoint: ```js @@ -2265,6 +2344,7 @@ paths: AJAX calls. Remember to send credentials and set up CORS correctly! Reverse proxies and API Gateways Server-side calls - use the `X-Session-Token` header! + Session refresh This endpoint authenticates users by checking diff --git a/internal/httpclient/api_default.go b/internal/httpclient/api_default.go new file mode 100644 index 000000000000..53af803048a9 --- /dev/null +++ b/internal/httpclient/api_default.go @@ -0,0 +1,183 @@ +/* + * Ory Kratos API + * + * Documentation for all public and administrative Ory Kratos APIs. Public and administrative APIs are exposed on different ports. Public APIs can face the public internet without any protection while administrative APIs should never be exposed without prior authorization. To protect the administative API port you should use something like Nginx, Ory Oathkeeper, or any other technology capable of authorizing incoming requests. + * + * API version: 1.0.0 + * Contact: hi@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "bytes" + "context" + "io/ioutil" + "net/http" + "net/url" +) + +// Linger please +var ( + _ context.Context +) + +type DefaultApi interface { + + /* + * V0alpha2 Calling this endpoint refreshes a current user session. If `session.refresh_time_window` is set it will only refresh the session after this time has passed. + * This endpoint is useful for: + + Session refresh + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @return DefaultApiApiV0alpha2Request + */ + V0alpha2(ctx context.Context) DefaultApiApiV0alpha2Request + + /* + * V0alpha2Execute executes the request + * @return SuccessfulAdminIdentitySession + */ + V0alpha2Execute(r DefaultApiApiV0alpha2Request) (*SuccessfulAdminIdentitySession, *http.Response, error) +} + +// DefaultApiService DefaultApi service +type DefaultApiService service + +type DefaultApiApiV0alpha2Request struct { + ctx context.Context + ApiService DefaultApi +} + +func (r DefaultApiApiV0alpha2Request) Execute() (*SuccessfulAdminIdentitySession, *http.Response, error) { + return r.ApiService.V0alpha2Execute(r) +} + +/* + * V0alpha2 Calling this endpoint refreshes a current user session. If `session.refresh_time_window` is set it will only refresh the session after this time has passed. + * This endpoint is useful for: + +Session refresh + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @return DefaultApiApiV0alpha2Request +*/ +func (a *DefaultApiService) V0alpha2(ctx context.Context) DefaultApiApiV0alpha2Request { + return DefaultApiApiV0alpha2Request{ + ApiService: a, + ctx: ctx, + } +} + +/* + * Execute executes the request + * @return SuccessfulAdminIdentitySession + */ +func (a *DefaultApiService) V0alpha2Execute(r DefaultApiApiV0alpha2Request) (*SuccessfulAdminIdentitySession, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodGet + localVarPostBody interface{} + localVarFormFileName string + localVarFileName string + localVarFileBytes []byte + localVarReturnValue *SuccessfulAdminIdentitySession + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "DefaultApiService.V0alpha2") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/sessions/refresh" + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + if r.ctx != nil { + // API Key Authentication + if auth, ok := r.ctx.Value(ContextAPIKeys).(map[string]APIKey); ok { + if apiKey, ok := auth["oryAccessToken"]; ok { + var key string + if apiKey.Prefix != "" { + key = apiKey.Prefix + " " + apiKey.Key + } else { + key = apiKey.Key + } + localVarHeaderParams["Authorization"] = key + } + } + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, localVarFormFileName, localVarFileName, localVarFileBytes) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := ioutil.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = ioutil.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + if localVarHTTPResponse.StatusCode == 404 { + var v JsonError + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 500 { + var v JsonError + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.model = v + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} diff --git a/internal/httpclient/api_v0alpha2.go b/internal/httpclient/api_v0alpha2.go index 48a1a2fb6331..ee9da5546368 100644 --- a/internal/httpclient/api_v0alpha2.go +++ b/internal/httpclient/api_v0alpha2.go @@ -142,6 +142,23 @@ type V0alpha2Api interface { */ AdminListIdentitiesExecute(r V0alpha2ApiApiAdminListIdentitiesRequest) ([]Identity, *http.Response, error) + /* + * AdminSessionRefresh Calling this endpoint refreshes a given session. If `session.refresh_time_window` is set it will only refresh the session after this time has passed. + * This endpoint is useful for: + + Session refresh + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @param id ID is the session's ID. + * @return V0alpha2ApiApiAdminSessionRefreshRequest + */ + AdminSessionRefresh(ctx context.Context, id string) V0alpha2ApiApiAdminSessionRefreshRequest + + /* + * AdminSessionRefreshExecute executes the request + * @return Session + */ + AdminSessionRefreshExecute(r V0alpha2ApiApiAdminSessionRefreshRequest) (*Session, *http.Response, error) + /* * AdminUpdateCredentials Update Identity Credentials * Calling this endpoint updates the credentials according to the specification provided. @@ -993,6 +1010,12 @@ type V0alpha2Api interface { Returns a session object in the body or 401 if the credentials are invalid or no credentials were sent. Additionally when the request it successful it adds the user ID to the 'X-Kratos-Authenticated-Identity-Id' header in the response. + It is also possible to refresh the session lifespan of a current session by adding a `refresh=true` param to the request url. + By default session refresh on this endpoint is disabled. + Session refresh can be enabled only after setting `session.whoami.refresh` to true in the config. + After enabling this option any refresh request will set the session life equal to `session.lifespan`. + If you want to refresh the session only some time before session expiration you can set a proper value for `session.refresh_time_window` + If you call this endpoint from a server-side application, you must forward the HTTP Cookie Header to this endpoint: ```js @@ -1024,6 +1047,7 @@ type V0alpha2Api interface { AJAX calls. Remember to send credentials and set up CORS correctly! Reverse proxies and API Gateways Server-side calls - use the `X-Session-Token` header! + Session refresh This endpoint authenticates users by checking @@ -2064,6 +2088,146 @@ func (a *V0alpha2ApiService) AdminListIdentitiesExecute(r V0alpha2ApiApiAdminLis return localVarReturnValue, localVarHTTPResponse, nil } +type V0alpha2ApiApiAdminSessionRefreshRequest struct { + ctx context.Context + ApiService V0alpha2Api + id string +} + +func (r V0alpha2ApiApiAdminSessionRefreshRequest) Execute() (*Session, *http.Response, error) { + return r.ApiService.AdminSessionRefreshExecute(r) +} + +/* + * AdminSessionRefresh Calling this endpoint refreshes a given session. If `session.refresh_time_window` is set it will only refresh the session after this time has passed. + * This endpoint is useful for: + +Session refresh + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @param id ID is the session's ID. + * @return V0alpha2ApiApiAdminSessionRefreshRequest +*/ +func (a *V0alpha2ApiService) AdminSessionRefresh(ctx context.Context, id string) V0alpha2ApiApiAdminSessionRefreshRequest { + return V0alpha2ApiApiAdminSessionRefreshRequest{ + ApiService: a, + ctx: ctx, + id: id, + } +} + +/* + * Execute executes the request + * @return Session + */ +func (a *V0alpha2ApiService) AdminSessionRefreshExecute(r V0alpha2ApiApiAdminSessionRefreshRequest) (*Session, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodPatch + localVarPostBody interface{} + localVarFormFileName string + localVarFileName string + localVarFileBytes []byte + localVarReturnValue *Session + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "V0alpha2ApiService.AdminSessionRefresh") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/sessions/refresh/{id}" + localVarPath = strings.Replace(localVarPath, "{"+"id"+"}", url.PathEscape(parameterToString(r.id, "")), -1) + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + if r.ctx != nil { + // API Key Authentication + if auth, ok := r.ctx.Value(ContextAPIKeys).(map[string]APIKey); ok { + if apiKey, ok := auth["oryAccessToken"]; ok { + var key string + if apiKey.Prefix != "" { + key = apiKey.Prefix + " " + apiKey.Key + } else { + key = apiKey.Key + } + localVarHeaderParams["Authorization"] = key + } + } + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, localVarFormFileName, localVarFileName, localVarFileBytes) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := ioutil.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = ioutil.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + if localVarHTTPResponse.StatusCode == 404 { + var v JsonError + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 500 { + var v JsonError + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.model = v + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} + type V0alpha2ApiApiAdminUpdateCredentialsRequest struct { ctx context.Context ApiService V0alpha2Api @@ -6515,6 +6679,12 @@ func (r V0alpha2ApiApiToSessionRequest) Execute() (*Session, *http.Response, err Returns a session object in the body or 401 if the credentials are invalid or no credentials were sent. Additionally when the request it successful it adds the user ID to the 'X-Kratos-Authenticated-Identity-Id' header in the response. +It is also possible to refresh the session lifespan of a current session by adding a `refresh=true` param to the request url. +By default session refresh on this endpoint is disabled. +Session refresh can be enabled only after setting `session.whoami.refresh` to true in the config. +After enabling this option any refresh request will set the session life equal to `session.lifespan`. +If you want to refresh the session only some time before session expiration you can set a proper value for `session.refresh_time_window` + If you call this endpoint from a server-side application, you must forward the HTTP Cookie Header to this endpoint: ```js @@ -6546,6 +6716,7 @@ This endpoint is useful for: AJAX calls. Remember to send credentials and set up CORS correctly! Reverse proxies and API Gateways Server-side calls - use the `X-Session-Token` header! +Session refresh This endpoint authenticates users by checking diff --git a/internal/httpclient/client.go b/internal/httpclient/client.go index 0469d3c1468c..cb1e4c59c9ac 100644 --- a/internal/httpclient/client.go +++ b/internal/httpclient/client.go @@ -49,6 +49,8 @@ type APIClient struct { // API Services + DefaultApi DefaultApi + MetadataApi MetadataApi V0alpha2Api V0alpha2Api @@ -70,6 +72,7 @@ func NewAPIClient(cfg *Configuration) *APIClient { c.common.client = c // API Services + c.DefaultApi = (*DefaultApiService)(&c.common) c.MetadataApi = (*MetadataApiService)(&c.common) c.V0alpha2Api = (*V0alpha2ApiService)(&c.common) diff --git a/internal/httpclient/docs/DefaultApi.md b/internal/httpclient/docs/DefaultApi.md new file mode 100644 index 000000000000..8c255417dc93 --- /dev/null +++ b/internal/httpclient/docs/DefaultApi.md @@ -0,0 +1,70 @@ +# \DefaultApi + +All URIs are relative to *http://localhost* + +Method | HTTP request | Description +------------- | ------------- | ------------- +[**V0alpha2**](DefaultApi.md#V0alpha2) | **Get** /sessions/refresh | Calling this endpoint refreshes a current user session. If `session.refresh_time_window` is set it will only refresh the session after this time has passed. + + + +## V0alpha2 + +> SuccessfulAdminIdentitySession V0alpha2(ctx).Execute() + +Calling this endpoint refreshes a current user session. If `session.refresh_time_window` is set it will only refresh the session after this time has passed. + + + +### Example + +```go +package main + +import ( + "context" + "fmt" + "os" + openapiclient "./openapi" +) + +func main() { + + configuration := openapiclient.NewConfiguration() + apiClient := openapiclient.NewAPIClient(configuration) + resp, r, err := apiClient.DefaultApi.V0alpha2(context.Background()).Execute() + if err != nil { + fmt.Fprintf(os.Stderr, "Error when calling `DefaultApi.V0alpha2``: %v\n", err) + fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r) + } + // response from `V0alpha2`: SuccessfulAdminIdentitySession + fmt.Fprintf(os.Stdout, "Response from `DefaultApi.V0alpha2`: %v\n", resp) +} +``` + +### Path Parameters + +This endpoint does not need any parameter. + +### Other Parameters + +Other parameters are passed through a pointer to a apiV0alpha2Request struct via the builder pattern + + +### Return type + +[**SuccessfulAdminIdentitySession**](SuccessfulAdminIdentitySession.md) + +### Authorization + +[oryAccessToken](../README.md#oryAccessToken) + +### HTTP request headers + +- **Content-Type**: Not defined +- **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) +[[Back to Model list]](../README.md#documentation-for-models) +[[Back to README]](../README.md) + diff --git a/internal/httpclient/docs/V0alpha2Api.md b/internal/httpclient/docs/V0alpha2Api.md index 5ac25231d2b7..cd5abf7ba5b1 100644 --- a/internal/httpclient/docs/V0alpha2Api.md +++ b/internal/httpclient/docs/V0alpha2Api.md @@ -11,6 +11,7 @@ Method | HTTP request | Description [**AdminGetIdentity**](V0alpha2Api.md#AdminGetIdentity) | **Get** /identities/{id} | Get an Identity [**AdminIdentitySession**](V0alpha2Api.md#AdminIdentitySession) | **Get** /identities/{id}/session | Calling this endpoint issues a session for a given identity. [**AdminListIdentities**](V0alpha2Api.md#AdminListIdentities) | **Get** /identities | List Identities +[**AdminSessionRefresh**](V0alpha2Api.md#AdminSessionRefresh) | **Patch** /sessions/refresh/{id} | Calling this endpoint refreshes a given session. If `session.refresh_time_window` is set it will only refresh the session after this time has passed. [**AdminUpdateCredentials**](V0alpha2Api.md#AdminUpdateCredentials) | **Put** /identities/{id}/credentials | Update Identity Credentials [**AdminUpdateIdentity**](V0alpha2Api.md#AdminUpdateIdentity) | **Put** /identities/{id} | Update an Identity [**CreateSelfServiceLogoutFlowUrlForBrowsers**](V0alpha2Api.md#CreateSelfServiceLogoutFlowUrlForBrowsers) | **Get** /self-service/logout/browser | Create a Logout URL for Browsers @@ -522,6 +523,76 @@ Name | Type | Description | Notes [[Back to README]](../README.md) +## AdminSessionRefresh + +> Session AdminSessionRefresh(ctx, id).Execute() + +Calling this endpoint refreshes a given session. If `session.refresh_time_window` is set it will only refresh the session after this time has passed. + + + +### Example + +```go +package main + +import ( + "context" + "fmt" + "os" + openapiclient "./openapi" +) + +func main() { + id := "id_example" // string | ID is the session's ID. + + configuration := openapiclient.NewConfiguration() + apiClient := openapiclient.NewAPIClient(configuration) + resp, r, err := apiClient.V0alpha2Api.AdminSessionRefresh(context.Background(), id).Execute() + if err != nil { + fmt.Fprintf(os.Stderr, "Error when calling `V0alpha2Api.AdminSessionRefresh``: %v\n", err) + fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r) + } + // response from `AdminSessionRefresh`: Session + fmt.Fprintf(os.Stdout, "Response from `V0alpha2Api.AdminSessionRefresh`: %v\n", resp) +} +``` + +### Path Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- +**ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. +**id** | **string** | ID is the session's ID. | + +### Other Parameters + +Other parameters are passed through a pointer to a apiAdminSessionRefreshRequest struct via the builder pattern + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + + +### Return type + +[**Session**](Session.md) + +### Authorization + +[oryAccessToken](../README.md#oryAccessToken) + +### HTTP request headers + +- **Content-Type**: Not defined +- **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) +[[Back to Model list]](../README.md#documentation-for-models) +[[Back to README]](../README.md) + + ## AdminUpdateCredentials > AdminUpdateCredentials(ctx, id).Execute() diff --git a/internal/testhelpers/handler_mock.go b/internal/testhelpers/handler_mock.go index 570e8b93eb1b..b52ffc0c88ee 100644 --- a/internal/testhelpers/handler_mock.go +++ b/internal/testhelpers/handler_mock.go @@ -92,19 +92,22 @@ func NewNoRedirectClientWithCookies(t *testing.T) *http.Client { } } -func MockHydrateCookieClient(t *testing.T, c *http.Client, u string) { +func MockHydrateCookieClient(t *testing.T, c *http.Client, u string) *http.Cookie { + var sessionCookie *http.Cookie res, err := c.Get(u) require.NoError(t, err) defer res.Body.Close() assert.EqualValues(t, http.StatusOK, res.StatusCode) var found bool - for _, c := range res.Cookies() { - if c.Name == config.DefaultSessionCookieName { + for _, rc := range res.Cookies() { + if rc.Name == config.DefaultSessionCookieName { found = true + sessionCookie = rc } } require.True(t, found) + return sessionCookie } func MockSessionCreateHandlerWithIdentity(t *testing.T, reg mockDeps, i *identity.Identity) (httprouter.Handle, *session.Session) { diff --git a/session/handler.go b/session/handler.go index b2fadeffa590..621b0b7a69ac 100644 --- a/session/handler.go +++ b/session/handler.go @@ -48,6 +48,8 @@ func NewHandler( const ( RouteCollection = "/sessions" RouteWhoami = RouteCollection + "/whoami" + RouteSessionRefresh = RouteCollection + "/refresh" + RouteSessionRefreshId = RouteSessionRefresh + "/:id" RouteIdentity = "/identities" RouteIdentityManagement = RouteIdentity + "/:id/sessions" RouteIdentitySession = RouteIdentity + "/:id/session" @@ -60,6 +62,8 @@ func (h *Handler) RegisterAdminRoutes(admin *x.RouterAdmin) { admin.Handle(m, RouteWhoami, x.RedirectToPublicRoute(h.r)) } admin.DELETE(RouteIdentityManagement, h.deleteIdentitySessions) + admin.PATCH(RouteSessionRefresh, h.adminCurrentSessionRefresh) + admin.PATCH(RouteSessionRefreshId, h.adminSessionRefresh) admin.GET(RouteIdentitySession, h.session) } @@ -104,6 +108,12 @@ type toSession struct { // Returns a session object in the body or 401 if the credentials are invalid or no credentials were sent. // Additionally when the request it successful it adds the user ID to the 'X-Kratos-Authenticated-Identity-Id' header in the response. // +// It is also possible to refresh the session lifespan of a current session by adding a `refresh=true` param to the request url. +// By default session refresh on this endpoint is disabled. +// Session refresh can be enabled only after setting `session.whoami.refresh_allowed` to true in the config. +// After enabling this option any refresh request will set the session life equal to `session.lifespan`. +// If you want to refresh the session only some time before session expiration you can set a proper value for `session.refresh_min_time_left` +// // If you call this endpoint from a server-side application, you must forward the HTTP Cookie Header to this endpoint: // // ```js @@ -135,6 +145,7 @@ type toSession struct { // - AJAX calls. Remember to send credentials and set up CORS correctly! // - Reverse proxies and API Gateways // - Server-side calls - use the `X-Session-Token` header! +// - Session refresh // // This endpoint authenticates users by checking // @@ -159,7 +170,7 @@ type toSession struct { // 401: jsonError // 403: jsonError // 500: jsonError -func (h *Handler) whoami(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { +func (h *Handler) whoami(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { s, err := h.r.SessionManager().FetchFromRequest(r.Context(), r) if err != nil { h.r.Audit().WithRequest(r).WithError(err).Info("No valid session cookie found.") @@ -168,7 +179,8 @@ func (h *Handler) whoami(w http.ResponseWriter, r *http.Request, ps httprouter.P } var aalErr *ErrAALNotSatisfied - if err := h.r.SessionManager().DoesSessionSatisfy(r, s, h.r.Config(r.Context()).SessionWhoAmIAAL()); errors.As(err, &aalErr) { + c := h.r.Config(r.Context()) + if err := h.r.SessionManager().DoesSessionSatisfy(r, s, c.SessionWhoAmIAAL()); errors.As(err, &aalErr) { h.r.Audit().WithRequest(r).WithError(err).Info("Session was found but AAL is not satisfied for calling this endpoint.") h.r.Writer().WriteError(w, r, err) return @@ -178,6 +190,17 @@ func (h *Handler) whoami(w http.ResponseWriter, r *http.Request, ps httprouter.P return } + // Refresh session if param was true + refresh := r.URL.Query().Get("refresh") + if c.SessionWhoAmIRefreshAllowed() && refresh == "true" && s.CanBeRefreshed(c) { + s = s.Refresh(c) + if err := h.r.SessionPersister().UpsertSession(r.Context(), s); err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + h.r.SessionManager().IssueCookie(r.Context(), w, r, s) + } + // s.Devices = nil s.Identity = s.Identity.CopyWithoutCredentials() @@ -314,6 +337,92 @@ func (h *Handler) session(w http.ResponseWriter, r *http.Request, ps httprouter. h.r.Writer().Write(w, r, &AdminIdentitySessionResponse{Session: s, Token: s.Token, Identity: i}) } +// swagger:parameters adminSessionRefresh +// nolint:deadcode,unused +type adminSessionRefresh struct { + // ID is the session's ID. + // + // required: true + // in: path + ID string `json:"id"` +} + +// swagger:route PATCH /sessions/refresh/{id} v0alpha2 adminSessionRefresh +// +// Calling this endpoint refreshes a given session. +// If `session.refresh_min_time_left` is set it will only refresh the session after this time has passed. +// +// This endpoint is useful for: +// +// - Session refresh +// +// Schemes: http, https +// +// Security: +// oryAccessToken: +// +// Responses: +// 200: session +// 404: jsonError +// 500: jsonError +func (h *Handler) adminSessionRefresh(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + iID, err := uuid.FromString(ps.ByName("id")) + if err != nil { + h.r.Writer().WriteError(w, r, herodot.ErrBadRequest.WithError(err.Error()).WithDebug("could not parse UUID")) + return + } + s, err := h.r.SessionPersister().GetSession(r.Context(), iID) + if err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + c := h.r.Config(r.Context()) + if s.CanBeRefreshed(c) { + if err := h.r.SessionPersister().UpsertSession(r.Context(), s.Refresh(c)); err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + } + + h.r.Writer().Write(w, r, s) +} + +// swagger:route GET /sessions/refresh v0alpha2 +// +// Calling this endpoint refreshes a current user session. +// If `session.refresh_min_time_left` is set it will only refresh the session after this time has passed. +// +// This endpoint is useful for: +// +// - Session refresh +// +// Schemes: http, https +// +// Security: +// oryAccessToken: +// +// Responses: +// 200: successfulAdminIdentitySession +// 404: jsonError +// 500: jsonError +func (h *Handler) adminCurrentSessionRefresh(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + s, err := h.r.SessionManager().FetchFromRequest(r.Context(), r) + if err != nil { + h.r.Audit().WithRequest(r).WithError(err).Info("No valid session cookie found.") + h.r.Writer().WriteError(w, r, herodot.ErrUnauthorized.WithWrap(err).WithReasonf("No valid session cookie found.")) + return + } + c := h.r.Config(r.Context()) + if s.CanBeRefreshed(c) { + if err := h.r.SessionPersister().UpsertSession(r.Context(), s.Refresh(c)); err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + } + + h.r.Writer().Write(w, r, s) +} + // fandom-end func (h *Handler) IsAuthenticated(wrap httprouter.Handle, onUnauthenticated httprouter.Handle) httprouter.Handle { diff --git a/session/handler_test.go b/session/handler_test.go index b4abb5a593a5..2b4eb2335221 100644 --- a/session/handler_test.go +++ b/session/handler_test.go @@ -5,7 +5,9 @@ import ( "encoding/json" "fmt" "net/http" + "net/http/cookiejar" "net/http/httptest" + "strings" "testing" "time" @@ -41,8 +43,27 @@ func send(code int) httprouter.Handle { } } -func assertNoCSRFCookieInResponse(t *testing.T, ts *httptest.Server, c *http.Client, res *http.Response) { - assert.Len(t, res.Cookies(), 0, res.Cookies()) +func getSessionCookie(t *testing.T, r *http.Response) *http.Cookie { + var sessionCookie *http.Cookie + var found bool + for _, c := range r.Cookies() { + if c.Name == config.DefaultSessionCookieName { + found = true + sessionCookie = c + } + } + require.True(t, found) + return sessionCookie +} + +func assertNoCSRFCookieInResponse(t *testing.T, _ *httptest.Server, _ *http.Client, r *http.Response) { + found := false + for _, c := range r.Cookies() { + if strings.HasPrefix(c.Name, "csrf_token") { + found = true + } + } + require.False(t, found) } func TestSessionWhoAmI(t *testing.T) { @@ -136,6 +157,37 @@ func TestSessionWhoAmI(t *testing.T) { } }) + t.Run("case=whoami refresh", func(t *testing.T) { + client := testhelpers.NewClientWithCookies(t) + conf.MustSet(config.ViperKeySessionWhoAmIRefreshAllowed, "true") + + // No cookie yet -> 401 + res, err := client.Get(ts.URL + RouteWhoami) + require.NoError(t, err) + assertNoCSRFCookieInResponse(t, ts, client, res) // Test that no CSRF cookie is ever set here. + + // Set cookie + reg.CSRFHandler().IgnorePath("/set") + originalCookie := testhelpers.MockHydrateCookieClient(t, client, ts.URL+"/set") + originalCookie.Expires = originalCookie.Expires.Add(-time.Second) + + // Cookie set -> 200 (GET) + req, err := http.NewRequest("GET", ts.URL+RouteWhoami+"?refresh=true", nil) + require.NoError(t, err) + + res, err = client.Do(req) + require.NoError(t, err) + assertNoCSRFCookieInResponse(t, ts, client, res) // Test that no CSRF cookie is ever set here. + + assert.EqualValues(t, http.StatusOK, res.StatusCode) + assert.NotEmpty(t, res.Header.Get("X-Kratos-Authenticated-Identity-Id")) + updatedCookie := getSessionCookie(t, res) + + require.NotEmpty(t, updatedCookie) + require.NotEqual(t, originalCookie.Expires, updatedCookie.Expires) + assert.True(t, originalCookie.Expires.Before(updatedCookie.Expires)) + }) + /* @@ -401,6 +453,107 @@ func TestHandlerDeleteSessionByIdentityID(t *testing.T) { }) } +func TestHandlerRefreshSessionByIdentityID(t *testing.T) { + conf, reg := internal.NewFastRegistryWithMocks(t) + _, ts, _, _ := testhelpers.NewKratosServerWithCSRFAndRouters(t, reg) + + // set this intermediate because kratos needs some valid url for CRUDE operations + conf.MustSet(config.ViperKeyPublicBaseURL, "http://example.com") + testhelpers.SetDefaultIdentitySchema(t, conf, "file://./stub/identity.schema.json") + conf.MustSet(config.ViperKeyPublicBaseURL, ts.URL) + + t.Run("case=should return 200 after refreshing one session", func(t *testing.T) { + client := testhelpers.NewClientWithCookies(t) + i := identity.NewIdentity("") + require.NoError(t, reg.IdentityManager().Create(context.Background(), i)) + s := &Session{Identity: i, ExpiresAt: time.Now().Add(5 * time.Minute)} + require.NoError(t, reg.SessionPersister().UpsertSession(context.Background(), s)) + + req, _ := http.NewRequest("PATCH", ts.URL+"/sessions/refresh/"+s.ID.String(), nil) + res, err := client.Do(req) + require.NoError(t, err) + require.Equal(t, http.StatusOK, res.StatusCode) + + s, err = reg.SessionPersister().GetSession(context.Background(), s.ID) + require.Nil(t, err) + }) + + t.Run("case=should return 400 when bad UUID is sent", func(t *testing.T) { + client := testhelpers.NewClientWithCookies(t) + req, _ := http.NewRequest("PATCH", ts.URL+"/sessions/refresh/BADUUID", nil) + res, err := client.Do(req) + require.NoError(t, err) + require.Equal(t, http.StatusBadRequest, res.StatusCode) + }) + + t.Run("case=should return 404 when calling with missing UUID", func(t *testing.T) { + client := testhelpers.NewClientWithCookies(t) + someID, _ := uuid.NewV4() + req, _ := http.NewRequest("PATCH", ts.URL+"/sessions/refresh/"+someID.String(), nil) + res, err := client.Do(req) + require.NoError(t, err) + require.Equal(t, http.StatusNotFound, res.StatusCode) + }) +} + +func TestHandlerRefreshCurrentSession(t *testing.T) { + conf, reg := internal.NewFastRegistryWithMocks(t) + + // Start kratos server + publicTS, adminTS, r, _ := testhelpers.NewKratosServerWithCSRFAndRouters(t, reg) + h, _ := testhelpers.MockSessionCreateHandler(t, reg) + r.GET("/set", h) + + mockServerURL := urlx.ParseOrPanic(publicTS.URL) + + adminTS.URL = strings.Replace(adminTS.URL, "127.0.0.1", "localhost", -1) + reg.Config(context.Background()).MustSet(config.ViperKeyAdminBaseURL, adminTS.URL) + testhelpers.SetDefaultIdentitySchema(t, conf, "file://./stub/identity.schema.json") + testhelpers.SetIdentitySchemas(t, conf, map[string]string{ + "customer": "file://./stub/handler/customer.schema.json", + "employee": "file://./stub/handler/employee.schema.json", + }) + //conf.MustSet(config.ViperKeyPublicBaseURL, mockServerURL.String()) + + client := testhelpers.NewClientWithCookies(t) + // Set cookie + reg.CSRFHandler().IgnorePath("/set") + originalCookie := testhelpers.MockHydrateCookieClient(t, client, publicTS.URL+"/set") + originalCookie.Expires = originalCookie.Expires.Add(-time.Second) + + session := func(t *testing.T, base *httptest.Server, href string, expectCode int) Session { + req, err := http.NewRequest("PATCH", base.URL+href, nil) + require.NoError(t, err) + cookies := client.Jar.Cookies(mockServerURL) + adminServerURL := urlx.ParseOrPanic(adminTS.URL) + cj, err := cookiejar.New(&cookiejar.Options{}) + require.NoError(t, err) + cj.SetCookies(adminServerURL, cookies) + base.Client().Jar = cj + + res, err := base.Client().Do(req) + require.NoError(t, err) + + require.EqualValues(t, expectCode, res.StatusCode) + defer res.Body.Close() + + var apiRes Session + err = json.NewDecoder(res.Body).Decode(&apiRes) + require.NoError(t, err) + fmt.Print(apiRes) + + return apiRes + } + + t.Run("case=should return 200 after successful session refresh and return valid session and token", func(t *testing.T) { + res := session(t, adminTS, "/sessions/refresh", http.StatusOK) + s, err := reg.SessionPersister().GetSession(context.Background(), res.ID) + require.Empty(t, err) + require.True(t, res.ExpiresAt.After(originalCookie.Expires)) + require.True(t, s.Active) + }) +} + func TestSessionRequest(t *testing.T) { conf, reg := internal.NewFastRegistryWithMocks(t) diff --git a/session/session.go b/session/session.go index a1713b19b99f..a40e66d0913b 100644 --- a/session/session.go +++ b/session/session.go @@ -24,6 +24,10 @@ type lifespanProvider interface { SessionLifespan() time.Duration } +type refreshWindowProvider interface { + SessionRefreshMinTimeLeft() time.Duration +} + // A Session // // swagger:model session @@ -156,6 +160,15 @@ func (s *Session) Declassify() *Session { return s } +func (s *Session) Refresh(c lifespanProvider) *Session { + s.ExpiresAt = time.Now().Add(c.SessionLifespan()) + return s +} + +func (s *Session) CanBeRefreshed(c refreshWindowProvider) bool { + return s.ExpiresAt.Add(-c.SessionRefreshMinTimeLeft()).Before(time.Now()) +} + func (s *Session) IsActive() bool { return s.Active && s.ExpiresAt.After(time.Now()) && (s.Identity == nil || s.Identity.IsActive()) } diff --git a/session/session_test.go b/session/session_test.go index 63bed7b20324..50cfd404a9e0 100644 --- a/session/session_test.go +++ b/session/session_test.go @@ -4,6 +4,8 @@ import ( "testing" "time" + "github.com/ory/kratos/driver/config" + "github.com/stretchr/testify/require" "github.com/stretchr/testify/assert" @@ -60,6 +62,23 @@ func TestSession(t *testing.T) { assert.Empty(t, s.AuthenticatedAt) }) + t.Run("case=session refresh", func(t *testing.T) { + conf.MustSet(config.ViperKeySessionLifespan, "24h") + conf.MustSet(config.ViperKeySessionRefreshMinTimeLeft, "12h") + t.Cleanup(func() { + conf.MustSet(config.ViperKeySessionLifespan, "1m") + conf.MustSet(config.ViperKeySessionRefreshMinTimeLeft, "1m") + }) + i := new(identity.Identity) + i.State = identity.StateActive + s, _ := session.NewActiveSession(i, conf, authAt, identity.CredentialsTypePassword) + assert.False(t, s.CanBeRefreshed(conf), "fresh session is not refreshable") + + s.ExpiresAt = s.ExpiresAt.Add(-12 * time.Hour) + assert.True(t, s.CanBeRefreshed(conf), "session is refreshable after 12hrs") + + }) + t.Run("case=aal", func(t *testing.T) { for _, tc := range []struct { d string diff --git a/spec/api.json b/spec/api.json index b1c4c35999f1..6a3d17f233ba 100755 --- a/spec/api.json +++ b/spec/api.json @@ -4332,9 +4332,111 @@ ] } }, + "/sessions/refresh": { + "get": { + "description": "This endpoint is useful for:\n\nSession refresh", + "operationId": "v0alpha2", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/successfulAdminIdentitySession" + } + } + }, + "description": "successfulAdminIdentitySession" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/jsonError" + } + } + }, + "description": "jsonError" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/jsonError" + } + } + }, + "description": "jsonError" + } + }, + "security": [ + { + "oryAccessToken": [] + } + ], + "summary": "Calling this endpoint refreshes a current user session.\nIf `session.refresh_time_window` is set it will only refresh the session after this time has passed." + } + }, + "/sessions/refresh/{id}": { + "patch": { + "description": "This endpoint is useful for:\n\nSession refresh", + "operationId": "adminSessionRefresh", + "parameters": [ + { + "description": "ID is the session's ID.", + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/session" + } + } + }, + "description": "session" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/jsonError" + } + } + }, + "description": "jsonError" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/jsonError" + } + } + }, + "description": "jsonError" + } + }, + "security": [ + { + "oryAccessToken": [] + } + ], + "summary": "Calling this endpoint refreshes a given session.\nIf `session.refresh_time_window` is set it will only refresh the session after this time has passed.", + "tags": [ + "v0alpha2" + ] + } + }, "/sessions/whoami": { "get": { - "description": "Uses the HTTP Headers in the GET request to determine (e.g. by using checking the cookies) who is authenticated.\nReturns a session object in the body or 401 if the credentials are invalid or no credentials were sent.\nAdditionally when the request it successful it adds the user ID to the 'X-Kratos-Authenticated-Identity-Id' header in the response.\n\nIf you call this endpoint from a server-side application, you must forward the HTTP Cookie Header to this endpoint:\n\n```js\npseudo-code example\nrouter.get('/protected-endpoint', async function (req, res) {\nconst session = await client.toSession(undefined, req.header('cookie'))\n\nconsole.log(session)\n})\n```\n\nWhen calling this endpoint from a non-browser application (e.g. mobile app) you must include the session token:\n\n```js\npseudo-code example\n...\nconst session = await client.toSession(\"the-session-token\")\n\nconsole.log(session)\n```\n\nDepending on your configuration this endpoint might return a 403 status code if the session has a lower Authenticator\nAssurance Level (AAL) than is possible for the identity. This can happen if the identity has password + webauthn\ncredentials (which would result in AAL2) but the session has only AAL1. If this error occurs, ask the user\nto sign in with the second factor or change the configuration.\n\nThis endpoint is useful for:\n\nAJAX calls. Remember to send credentials and set up CORS correctly!\nReverse proxies and API Gateways\nServer-side calls - use the `X-Session-Token` header!\n\nThis endpoint authenticates users by checking\n\nif the `Cookie` HTTP header was set containing an Ory Kratos Session Cookie;\nif the `Authorization: bearer \u003cory-session-token\u003e` HTTP header was set with a valid Ory Kratos Session Token;\nif the `X-Session-Token` HTTP header was set with a valid Ory Kratos Session Token.\n\nIf none of these headers are set or the cooke or token are invalid, the endpoint returns a HTTP 401 status code.\n\nAs explained above, this request may fail due to several reasons. The `error.id` can be one of:\n\n`session_inactive`: No active session was found in the request (e.g. no Ory Session Cookie / Ory Session Token).\n`session_aal2_required`: An active session was found but it does not fulfil the Authenticator Assurance Level, implying that the session must (e.g.) authenticate the second factor.", + "description": "Uses the HTTP Headers in the GET request to determine (e.g. by using checking the cookies) who is authenticated.\nReturns a session object in the body or 401 if the credentials are invalid or no credentials were sent.\nAdditionally when the request it successful it adds the user ID to the 'X-Kratos-Authenticated-Identity-Id' header in the response.\n\nIt is also possible to refresh the session lifespan of a current session by adding a `refresh=true` param to the request url.\nBy default session refresh on this endpoint is disabled.\nSession refresh can be enabled only after setting `session.whoami.refresh` to true in the config.\nAfter enabling this option any refresh request will set the session life equal to `session.lifespan`.\nIf you want to refresh the session only some time before session expiration you can set a proper value for `session.refresh_time_window`\n\nIf you call this endpoint from a server-side application, you must forward the HTTP Cookie Header to this endpoint:\n\n```js\npseudo-code example\nrouter.get('/protected-endpoint', async function (req, res) {\nconst session = await client.toSession(undefined, req.header('cookie'))\n\nconsole.log(session)\n})\n```\n\nWhen calling this endpoint from a non-browser application (e.g. mobile app) you must include the session token:\n\n```js\npseudo-code example\n...\nconst session = await client.toSession(\"the-session-token\")\n\nconsole.log(session)\n```\n\nDepending on your configuration this endpoint might return a 403 status code if the session has a lower Authenticator\nAssurance Level (AAL) than is possible for the identity. This can happen if the identity has password + webauthn\ncredentials (which would result in AAL2) but the session has only AAL1. If this error occurs, ask the user\nto sign in with the second factor or change the configuration.\n\nThis endpoint is useful for:\n\nAJAX calls. Remember to send credentials and set up CORS correctly!\nReverse proxies and API Gateways\nServer-side calls - use the `X-Session-Token` header!\nSession refresh\n\nThis endpoint authenticates users by checking\n\nif the `Cookie` HTTP header was set containing an Ory Kratos Session Cookie;\nif the `Authorization: bearer \u003cory-session-token\u003e` HTTP header was set with a valid Ory Kratos Session Token;\nif the `X-Session-Token` HTTP header was set with a valid Ory Kratos Session Token.\n\nIf none of these headers are set or the cooke or token are invalid, the endpoint returns a HTTP 401 status code.\n\nAs explained above, this request may fail due to several reasons. The `error.id` can be one of:\n\n`session_inactive`: No active session was found in the request (e.g. no Ory Session Cookie / Ory Session Token).\n`session_aal2_required`: An active session was found but it does not fulfil the Authenticator Assurance Level, implying that the session must (e.g.) authenticate the second factor.", "operationId": "toSession", "parameters": [ { diff --git a/spec/swagger.json b/spec/swagger.json index 33b58cb2c569..5b96a8187b71 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -2005,9 +2005,93 @@ } } }, + "/sessions/refresh": { + "get": { + "security": [ + { + "oryAccessToken": [] + } + ], + "description": "This endpoint is useful for:\n\nSession refresh", + "schemes": [ + "http", + "https" + ], + "summary": "Calling this endpoint refreshes a current user session.\nIf `session.refresh_time_window` is set it will only refresh the session after this time has passed.", + "operationId": "v0alpha2", + "responses": { + "200": { + "description": "successfulAdminIdentitySession", + "schema": { + "$ref": "#/definitions/successfulAdminIdentitySession" + } + }, + "404": { + "description": "jsonError", + "schema": { + "$ref": "#/definitions/jsonError" + } + }, + "500": { + "description": "jsonError", + "schema": { + "$ref": "#/definitions/jsonError" + } + } + } + } + }, + "/sessions/refresh/{id}": { + "patch": { + "security": [ + { + "oryAccessToken": [] + } + ], + "description": "This endpoint is useful for:\n\nSession refresh", + "schemes": [ + "http", + "https" + ], + "tags": [ + "v0alpha2" + ], + "summary": "Calling this endpoint refreshes a given session.\nIf `session.refresh_time_window` is set it will only refresh the session after this time has passed.", + "operationId": "adminSessionRefresh", + "parameters": [ + { + "type": "string", + "description": "ID is the session's ID.", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "session", + "schema": { + "$ref": "#/definitions/session" + } + }, + "404": { + "description": "jsonError", + "schema": { + "$ref": "#/definitions/jsonError" + } + }, + "500": { + "description": "jsonError", + "schema": { + "$ref": "#/definitions/jsonError" + } + } + } + } + }, "/sessions/whoami": { "get": { - "description": "Uses the HTTP Headers in the GET request to determine (e.g. by using checking the cookies) who is authenticated.\nReturns a session object in the body or 401 if the credentials are invalid or no credentials were sent.\nAdditionally when the request it successful it adds the user ID to the 'X-Kratos-Authenticated-Identity-Id' header in the response.\n\nIf you call this endpoint from a server-side application, you must forward the HTTP Cookie Header to this endpoint:\n\n```js\npseudo-code example\nrouter.get('/protected-endpoint', async function (req, res) {\nconst session = await client.toSession(undefined, req.header('cookie'))\n\nconsole.log(session)\n})\n```\n\nWhen calling this endpoint from a non-browser application (e.g. mobile app) you must include the session token:\n\n```js\npseudo-code example\n...\nconst session = await client.toSession(\"the-session-token\")\n\nconsole.log(session)\n```\n\nDepending on your configuration this endpoint might return a 403 status code if the session has a lower Authenticator\nAssurance Level (AAL) than is possible for the identity. This can happen if the identity has password + webauthn\ncredentials (which would result in AAL2) but the session has only AAL1. If this error occurs, ask the user\nto sign in with the second factor or change the configuration.\n\nThis endpoint is useful for:\n\nAJAX calls. Remember to send credentials and set up CORS correctly!\nReverse proxies and API Gateways\nServer-side calls - use the `X-Session-Token` header!\n\nThis endpoint authenticates users by checking\n\nif the `Cookie` HTTP header was set containing an Ory Kratos Session Cookie;\nif the `Authorization: bearer \u003cory-session-token\u003e` HTTP header was set with a valid Ory Kratos Session Token;\nif the `X-Session-Token` HTTP header was set with a valid Ory Kratos Session Token.\n\nIf none of these headers are set or the cooke or token are invalid, the endpoint returns a HTTP 401 status code.\n\nAs explained above, this request may fail due to several reasons. The `error.id` can be one of:\n\n`session_inactive`: No active session was found in the request (e.g. no Ory Session Cookie / Ory Session Token).\n`session_aal2_required`: An active session was found but it does not fulfil the Authenticator Assurance Level, implying that the session must (e.g.) authenticate the second factor.", + "description": "Uses the HTTP Headers in the GET request to determine (e.g. by using checking the cookies) who is authenticated.\nReturns a session object in the body or 401 if the credentials are invalid or no credentials were sent.\nAdditionally when the request it successful it adds the user ID to the 'X-Kratos-Authenticated-Identity-Id' header in the response.\n\nIt is also possible to refresh the session lifespan of a current session by adding a `refresh=true` param to the request url.\nBy default session refresh on this endpoint is disabled.\nSession refresh can be enabled only after setting `session.whoami.refresh` to true in the config.\nAfter enabling this option any refresh request will set the session life equal to `session.lifespan`.\nIf you want to refresh the session only some time before session expiration you can set a proper value for `session.refresh_time_window`\n\nIf you call this endpoint from a server-side application, you must forward the HTTP Cookie Header to this endpoint:\n\n```js\npseudo-code example\nrouter.get('/protected-endpoint', async function (req, res) {\nconst session = await client.toSession(undefined, req.header('cookie'))\n\nconsole.log(session)\n})\n```\n\nWhen calling this endpoint from a non-browser application (e.g. mobile app) you must include the session token:\n\n```js\npseudo-code example\n...\nconst session = await client.toSession(\"the-session-token\")\n\nconsole.log(session)\n```\n\nDepending on your configuration this endpoint might return a 403 status code if the session has a lower Authenticator\nAssurance Level (AAL) than is possible for the identity. This can happen if the identity has password + webauthn\ncredentials (which would result in AAL2) but the session has only AAL1. If this error occurs, ask the user\nto sign in with the second factor or change the configuration.\n\nThis endpoint is useful for:\n\nAJAX calls. Remember to send credentials and set up CORS correctly!\nReverse proxies and API Gateways\nServer-side calls - use the `X-Session-Token` header!\nSession refresh\n\nThis endpoint authenticates users by checking\n\nif the `Cookie` HTTP header was set containing an Ory Kratos Session Cookie;\nif the `Authorization: bearer \u003cory-session-token\u003e` HTTP header was set with a valid Ory Kratos Session Token;\nif the `X-Session-Token` HTTP header was set with a valid Ory Kratos Session Token.\n\nIf none of these headers are set or the cooke or token are invalid, the endpoint returns a HTTP 401 status code.\n\nAs explained above, this request may fail due to several reasons. The `error.id` can be one of:\n\n`session_inactive`: No active session was found in the request (e.g. no Ory Session Cookie / Ory Session Token).\n`session_aal2_required`: An active session was found but it does not fulfil the Authenticator Assurance Level, implying that the session must (e.g.) authenticate the second factor.", "produces": [ "application/json" ],