[OAuth2] Infrastructure for tracking token expiry time
Hi Hackers,
Currently, during OAuth2 authentication, the ValidatorModuleResult
structure allows a validator(extension) to return the authentication status
and the authn_id.
However, we ignore the token expiry time (exp claim).
Once a token is validated, the backend has no record of when that token
actually expires. A session can remain open indefinitely even if the
underlying access token has expired shortly after the connection was
established.
This patch adds the infrastructure to capture and store this expiration
timestamp within the backend session state. It does not implement an
enforcement policy (such as auto-termination).
Request a review.
Thanks & Best Regards,
Ajit
Attachments:
password_expiry_oauth.diffapplication/octet-stream; name=password_expiry_oauth.diffDownload+21-0
On Mon, Feb 16, 2026 at 3:44 PM Ajit Awekar <ajitpostgres@gmail.com> wrote:
Hi Hackers,
Currently, during OAuth2 authentication, the ValidatorModuleResult
structure allows a validator(extension) to return the authentication status
and the authn_id.
However, we ignore the token expiry time (exp claim).Once a token is validated, the backend has no record of when that token
actually expires. A session can remain open indefinitely even if the
underlying access token has expired shortly after the connection was
established.This patch adds the infrastructure to capture and store this expiration
timestamp within the backend session state. It does not implement an
enforcement policy (such as auto-termination).
Hi Ajit,
Thanks for working on this. Storing the token expiry in the backend session
state makes sense as groundwork for future enforcement.
I had a couple of questions while reading the patch.
First, is Port always zero-initialized? If not, we might want to explicitly
initialize the new expiry field to a known value. Right now it looks like
we’re relying on zero to mean “not provided”, but since TimestampTz value 0
is a valid timestamp (Postgres epoch), I’m wondering whether it would be
clearer to use an explicit invalid/sentinel value instead.
Also, in the case where the validator returns an expiry that is already in
the past, should we reject the authentication immediately? Or is that
expected to be fully handled inside the validator module?
Finally, do you have a particular enforcement model in mind for follow-up
work (e.g., check at statement start, transaction boundaries, or via some
timeout mechanism)? It would help to understand how you see this being used.
The change itself looks straightforward, just trying to clarify the
intended semantics.
Best regards,
Vasuki M
C-DAC,Chennai.
Hello
This API looks simple for providers that use JWT access tokens, but
what about providers that use opaque tokens and an introspection API
to check validity instead? Some validators might not be able to
provide anything meaningful without a periodic call to a "check
validity now" method, and even some providers that use JWT access
tokens support immediate revocation, where these periodic checks would
be useful.
Thanks Vasuki and Zsolt for your reply and comments.
First, is Port always zero-initialized? If not, we might want to
explicitly initialize the new expiry field to a known value. Right now it
looks like we’re relying on zero to mean “not provided”, but since
TimestampTz value 0 is a valid timestamp (Postgres epoch), I’m wondering
whether it would be clearer to use an explicit invalid/sentinel value
instead.
I agree. The attached patch value is now initialised to
sentinel DT_NOBEGIN to indicate no expiry value has been provided yet.
Also, in the case where the validator returns an expiry that is already
in the past, should we reject the authentication immediately? Or is that
expected to be fully handled inside the validator module?
The design assumes that the Validator module will handle the immediate
rejection of tokens already in the past. The expiry field is intended for
the backend to manage session life after successful authentication
Finally, do you have a particular enforcement model in mind for
follow-up work (e.g., check at statement start, transaction boundaries, or
via some timeout mechanism)? It would help to understand how you see this
being used.
Ideally we should check this at statement start.
This API looks simple for providers that use JWT access tokens, but
what about providers that use opaque tokens and an introspection API
to check validity instead?
For providers using opaque tokens or introspection APIs where an 'exp'
claim might be missing, the API remains compatible by allowing the
validator to return DT_NOBEGIN.
Request a review.
Thanks & Best Regards,
Ajit
On Tue, 17 Feb 2026 at 01:10, Zsolt Parragi <zsolt.parragi@percona.com>
wrote:
Show quoted text
Hello
This API looks simple for providers that use JWT access tokens, but
what about providers that use opaque tokens and an introspection API
to check validity instead? Some validators might not be able to
provide anything meaningful without a periodic call to a "check
validity now" method, and even some providers that use JWT access
tokens support immediate revocation, where these periodic checks would
be useful.
Attachments:
password_expiry_oauth_V1.difftext/x-patch; charset=US-ASCII; name=password_expiry_oauth_V1.diffDownload+28-0
For providers using opaque tokens or introspection APIs where an 'exp' claim might be missing, the API remains compatible by allowing the validator to return DT_NOBEGIN.
I don't think this is a good answer: the OAuth validator API went to
the trouble of introducing a generic infrastructure with the explicit
goal to work any OAuth provider, and now this proposes a change that
limits a new API to some of them, while it wouldn't be more difficult
to propose a generic API that works for everything.
Let me rephrase the question: why is this a better approach than
introducing an additional validator callback method, expired_cb?
* it returns if the current OAuth token is expired or not
* if it's NULL, nothing happens, so there's an easy upgrade path for
validator in PG19
* for JWT validators with a clear expiry date, all they have to do is
to store the expiry date in a global variable and then check if we
passed that time in the new callback
* alternatively, this callback could return the current expected
expiry date, and the calling code could check it, but I think that's
overcomplicating
And in both cases, I think handling of the value/callback should be
part of the patch - only providing an API and then doing nothing with
it would set wrong expectations.
Hi All,
I see the concern about keeping the validator API generic and not
implicitly favoring JWT-style providers.
The callback-based approach does seem more flexible, especially for opaque
tokens or providers supporting revocation, where validity cannot be
represented as a fixed timestamp.
Perhaps one possible direction could be to support both:
An optional expiry timestamp for simple/static cases.
An optional callback (e.g., expired_cb) for dynamic validation.
This would allow JWT-based validators to remain lightweight while enabling
more complex providers to implement custom revalidation logic.
If enforcement is planned at statement start, integrating the callback
mechanism in the same patch might also clarify the intended semantics.
Best regards,
Vasuki M
C-DAC,Chennai
Hi Vasuki, Zsolt
Thanks a lot for your review comments and reply.I have updated the patch
and below is summary of changes
1. Adding a check_oauth_expiry() function called during command
execution to verify token validity
2. Terminating sessions with expired/revoked tokens before executing new
commands.
3. Supporting callback-based revocation checks
I have added a unit test case to validate that sessions are properly
terminated when their OAuth tokens expire.
Request a review.
Thanks & Best Regards,
Ajit
On Tue, 17 Feb 2026 at 16:17, VASUKI M <vasukianand0119@gmail.com> wrote:
Show quoted text
Hi All,
I see the concern about keeping the validator API generic and not
implicitly favoring JWT-style providers.
The callback-based approach does seem more flexible, especially for opaque
tokens or providers supporting revocation, where validity cannot be
represented as a fixed timestamp.
Perhaps one possible direction could be to support both:An optional expiry timestamp for simple/static cases.
An optional callback (e.g., expired_cb) for dynamic validation.
This would allow JWT-based validators to remain lightweight while enabling
more complex providers to implement custom revalidation logic.
If enforcement is planned at statement start, integrating the callback
mechanism in the same patch might also clarify the intended semantics.Best regards,
Vasuki M
C-DAC,Chennai
Attachments:
password_expiry_oauth_V2.patchapplication/octet-stream; name=password_expiry_oauth_V2.patchDownload+299-1
On 18 Feb 2026, at 09:38, Ajit Awekar <ajitpostgres@gmail.com> wrote:
I have added a unit test case to validate that sessions are properly terminated when their OAuth tokens expire.
+ /*
+ * If the current session was authenticated via OAuth, verify that the
+ * token has not expired or been revoked before executing the query.
+ */
+ if (MyClientConnectionInfo.auth_method == uaOAuth)
+ check_oauth_expiry(MyProcPort);
This seems too expensive and invasive in these codepaths and also not in line
with how other authentication methods are handled, we for example don't check
if a client certificate has been revoked in a similar manner. I don't think it
would be bad if we could detect expired credentials mid-flight but I think it
would need to be done smarter than sprinkling auth specific checks like this.
+ ereport(FATAL,
+ (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION),
+ errmsg("session expired: OAuth token is no longer valid")));
Token expiration is IMHO not a usecase for a FATAL error, if we want to
terminate a connection we can do it in a more graceful way.
+ /*
+ * Optional callback to check if the session is still valid.
+ * Returns true if the token is expired/revoked, false otherwise.
+ * If NULL, the backend assumes the session never expires.
+ * If provided, the validator can use this to limit session duration based on
+ * parameter value or based on it's custom logic.
+ */
+ bool (*expired_cb) (TimestampTz expiry);
+
+ /*
+ * The expiration time of the token (e.g., from the 'exp' claim) if
+ * provided. This value is passed as an argument to the expired_cb function
+ * above to determine if the session should terminate.
+ */
+ TimestampTz expiry;
} ValidatorModuleResult;
What's the point of having an expiry timestamp as well as an expiration check
callback? Wouldn't that open up for the caller to make its own judgment on
expiration which might conflict with expired_cb? Is token expiration really
always tied to a timestamp for all types of tokens?
Also, why is this defined in ValidatorModuleResult? If I interpret Zsolt's
comment upthread correctly it was meant to be placed in OAuthValidatorCallbacks
as a new callback - which I agree with would be better. The question of where
and when to invoke it remains, but whatever we build I think it should be auth
agnostic so that any auth can be hooked into it.
--
Daniel Gustafsson
Also, why is this defined in ValidatorModuleResult? If I interpret Zsolt's
comment upthread correctly it was meant to be placed in OAuthValidatorCallbacks
as a new callback - which I agree with would be better. The question of where
and when to invoke it remains, but whatever we build I think it should be auth
agnostic so that any auth can be hooked into it.
+1 Yes, that's what I meant. Also agree with the unnecessary expiry
timestamp with the callback - if we want to store some additional data
for the checks, that should be a void*, but with the single threaded
model there's no real need for it, the validator can store anything it
needs in global variables.
2. Terminating sessions with expired/revoked tokens before executing new
commands.
Token expiration is IMHO not a use case for a FATAL error, if we want to
terminate a connection we can do it in a more graceful way.
There are different reasons for token expiration, one is a simple
timeout where all we have to do is communicate to the client that we
need a refresh (gracefully), and the other is where a token is
immediately revoked because of a security incident, in which case
immediate termination is a good practice.
There was an earlier discussion about it in the password expiration
thread[1]/messages/by-id/CAER375OvH3_ONmc-SgUFpA6gv_d6eNj2KdZktzo-f_uqNwwWNw@mail.gmail.com, and the possible use of the GoAway[2]/messages/by-id/DDPQ1RV5FE9U.I2WW34NGRD8Z@jeltef.nl for it. Jacob
suggested making it user configurable, which seems like a reasonable
way to do it.
I also wanted to work on this patch, but I didn't start working on it
so far, because I wanted to see where those threads go, as the changes
introduced in them could be useful for the oauth token
refresh/disconnection mechanism.
[1]: /messages/by-id/CAER375OvH3_ONmc-SgUFpA6gv_d6eNj2KdZktzo-f_uqNwwWNw@mail.gmail.com
[2]: /messages/by-id/DDPQ1RV5FE9U.I2WW34NGRD8Z@jeltef.nl
On 18 Feb 2026, at 13:04, Zsolt Parragi <zsolt.parragi@percona.com> wrote:
2. Terminating sessions with expired/revoked tokens before executing new
commands.Token expiration is IMHO not a use case for a FATAL error, if we want to
terminate a connection we can do it in a more graceful way.There are different reasons for token expiration, one is a simple
timeout where all we have to do is communicate to the client that we
need a refresh (gracefully), and the other is where a token is
immediately revoked because of a security incident, in which case
immediate termination is a good practice.
I understand these cases and agree that there are different needs for messaging
to the user for these cases, but I still think that neither should overload
what FATAL error means. The mechanism used is however a secondary discussion,
first thing to get in place is a design for how to handle mid-connection
credential expiration.
--
Daniel Gustafsson
but I still think that neither should overload
what FATAL error means
I see, I misunderstood what you meant by graceful there. In this case,
this is also a good comment for the password expiration thread,
currently that also uses FATAL errors for terminating a connection
when the password expires.
What other option do you see? Something new for this use case like
GoAway, and clients not understanding it simply get disconnected after
some grace period? Or using the recently merged connectionWarning to
send a warning to the client, and disconnect it shortly if it doesn't
do anything to fix the situation?
When I tested the password expiration patch I noticed that deleted
users who still have remaining active connections currently get ERRORs
for every statement that requires permission checks, so in this regard
using ERROR/FATAL for the situation seemed fine to me - it's similar
to what already happens in some edge cases with authentication.
Thanks a lot Daniel, Zslot, Vasuki for your review comments.
The mechanism used is however a secondary discussion,
first thing to get in place is a design for how to handle mid-connection
credential expiration.
This patch introduces a generic credential validation framework that allows
us to periodically validate authentication credentials during active
database sessions. When enabled, this feature detects expired
credentials and terminates sessions that are no longer valid.
Added GUCs
Credential_validation.enabled = on // Enable or Disable Credential
validation
Credential_validation.interval = 120 //Frequency in seconds of running
credential validation
The callback mechanism works by:
- Defining a CredentialValidationCallback function pointer type
- Maintaining an array of validators indexed by authentication method
- Allowing other auth mechanisms to register validators via
RegisterCredentialValidator()
- Selecting the appropriate validator at runtime based on the session's
authentication method
The current implementation primarily supports password-based authentication
methods, verifying that passwords haven't expired. It can be extended to
any authentication method.
This patch is WIP. I am submitting it now to get early feedback on the
overall design and approach.
Thanks & Best Regards,
Ajit
On Wed, 18 Feb 2026 at 22:29, Zsolt Parragi <zsolt.parragi@percona.com>
wrote:
Show quoted text
but I still think that neither should overload
what FATAL error meansI see, I misunderstood what you meant by graceful there. In this case,
this is also a good comment for the password expiration thread,
currently that also uses FATAL errors for terminating a connection
when the password expires.What other option do you see? Something new for this use case like
GoAway, and clients not understanding it simply get disconnected after
some grace period? Or using the recently merged connectionWarning to
send a warning to the client, and disconnect it shortly if it doesn't
do anything to fix the situation?When I tested the password expiration patch I noticed that deleted
users who still have remaining active connections currently get ERRORs
for every statement that requires permission checks, so in this regard
using ERROR/FATAL for the situation seemed fine to me - it's similar
to what already happens in some edge cases with authentication.