@@ -1500,3 +1500,188 @@ func createTestRequest(id string) *fosite.Request {
15001500 Session : & oauth2.Session {DefaultSession : & openid.DefaultSession {Subject : "bar" }},
15011501 }
15021502}
1503+
1504+ func testHelperRefreshTokenExpiryUpdate (x oauth2.InternalRegistry ) func (t * testing.T ) {
1505+ return func (t * testing.T ) {
1506+ ctx := context .Background ()
1507+
1508+ // Create client
1509+ cl := & client.Client {ID : "refresh-expiry-client" }
1510+ require .NoError (t , x .ClientManager ().CreateClient (ctx , cl ))
1511+
1512+ // Create a request with a long expiry
1513+ initialRequest := fosite.Request {
1514+ ID : uuid .New (),
1515+ RequestedAt : time .Now ().UTC ().Round (time .Second ),
1516+ Client : cl ,
1517+ Session : oauth2 .NewSession ("sub" ),
1518+ }
1519+
1520+ // Set a long expiry time (24 hours)
1521+ initialExpiry := time .Now ().Add (24 * time .Hour )
1522+ initialRequest .Session .SetExpiresAt (fosite .RefreshToken , initialExpiry )
1523+
1524+ t .Run ("regular rotation" , func (t * testing.T ) {
1525+ // Create original refresh token
1526+ regularSignature := uuid .New ()
1527+ require .NoError (t , x .OAuth2Storage ().CreateRefreshTokenSession (ctx , regularSignature , "" , & initialRequest ))
1528+
1529+ // Verify initial expiry is set correctly
1530+ originalToken , err := x .OAuth2Storage ().GetRefreshTokenSession (ctx , regularSignature , oauth2 .NewSession ("sub" ))
1531+ require .NoError (t , err )
1532+ require .Equal (t , initialExpiry .Unix (), originalToken .GetSession ().GetExpiresAt (fosite .RefreshToken ).Unix ())
1533+
1534+ // Set up a connection to directly query the database
1535+ var actualExpiresAt time.Time
1536+ require .NoError (t , x .Persister ().Connection (ctx ).RawQuery ("SELECT expires_at FROM hydra_oauth2_refresh WHERE signature=?" , regularSignature ).First (& actualExpiresAt ))
1537+ require .Equal (t , initialExpiry .UTC ().Round (time .Second ), actualExpiresAt .UTC ().Round (time .Second ))
1538+
1539+ // Rotate the token
1540+ err = x .OAuth2Storage ().RotateRefreshToken (ctx , initialRequest .ID , regularSignature )
1541+ require .NoError (t , err )
1542+
1543+ // Check that the original token's expiry was updated to be closer to now
1544+ var revokedData struct {
1545+ ExpiresAt time.Time `db:"expires_at"`
1546+ Active bool `db:"active"`
1547+ }
1548+ require .NoError (t , x .Persister ().Connection (ctx ).RawQuery ("SELECT expires_at, active FROM hydra_oauth2_refresh WHERE signature=?" , regularSignature ).First (& revokedData ))
1549+
1550+ // Verify the token is now inactive
1551+ require .False (t , revokedData .Active )
1552+
1553+ // Verify the expiry is updated to be closer to now than the original expiry
1554+ require .True (t , revokedData .ExpiresAt .Before (initialExpiry ), "Expiry should be updated to be sooner than original" )
1555+ require .True (t , revokedData .ExpiresAt .After (time .Now ()), "Expiry should still be in the future" )
1556+ require .True (t , time .Until (revokedData .ExpiresAt ) < time .Until (initialExpiry ), "New expiry should be closer to now than original expiry" )
1557+
1558+ t .Logf ("Original expiry: %v, Updated expiry: %v, Now: %v" , initialExpiry , revokedData .ExpiresAt , time .Now ())
1559+ })
1560+
1561+ t .Run ("graceful rotation" , func (t * testing.T ) {
1562+ // Create refresh token for graceful rotation
1563+ gracefulSignature := uuid .New ()
1564+ require .NoError (t , x .OAuth2Storage ().CreateRefreshTokenSession (ctx , gracefulSignature , "" , & initialRequest ))
1565+
1566+ // Set config to graceful rotation
1567+ oldPeriod := x .Config ().GracefulRefreshTokenRotation (ctx ).Period
1568+ t .Cleanup (func () {
1569+ x .Config ().MustSet (ctx , config .KeyRefreshTokenRotationGracePeriod , oldPeriod )
1570+ x .Config ().MustSet (ctx , config .KeyRefreshTokenRotationGraceReuseCount , 0 )
1571+ })
1572+ x .Config ().MustSet (ctx , config .KeyRefreshTokenRotationGracePeriod , time .Minute * 30 )
1573+ x .Config ().MustSet (ctx , config .KeyRefreshTokenRotationGraceReuseCount , 3 )
1574+
1575+ // Record time before rotation
1576+ beforeRotation := time .Now ().UTC ().Add (- time .Second ) // Ensure we have a different timestamp for first_used_at
1577+
1578+ // Rotate the token
1579+ err := x .OAuth2Storage ().RotateRefreshToken (ctx , initialRequest .ID , gracefulSignature )
1580+ require .NoError (t , err )
1581+
1582+ // Check the token's expiry and status
1583+ var rotatedData struct {
1584+ ExpiresAt time.Time `db:"expires_at"`
1585+ Active bool `db:"active"`
1586+ FirstUsedAt sqlxx.NullTime `db:"first_used_at"`
1587+ UsedTimes sqlxx.NullInt64 `db:"used_times"`
1588+ }
1589+ require .NoError (t , x .Persister ().Connection (ctx ).RawQuery ("SELECT expires_at, active, first_used_at, used_times FROM hydra_oauth2_refresh WHERE signature=?" , gracefulSignature ).First (& rotatedData ))
1590+
1591+ // Token is used
1592+ require .False (t , rotatedData .Active )
1593+
1594+ // Verify first_used_at is set and reasonable
1595+ assert .True (t , time .Time (rotatedData .FirstUsedAt ).After (beforeRotation ) || time .Time (rotatedData .FirstUsedAt ).Equal (beforeRotation ), "%s should be after or equal to %s" , time .Time (rotatedData .FirstUsedAt ), beforeRotation )
1596+
1597+ now := time .Now ().UTC ().Add (time .Second )
1598+ assert .True (t , time .Time (rotatedData .FirstUsedAt ).Before (now ) || time .Time (rotatedData .FirstUsedAt ).Equal (now ), "%s should be before or equal to %s" , time .Time (rotatedData .FirstUsedAt ), now )
1599+
1600+ // Verify used_times was incremented
1601+ assert .True (t , rotatedData .UsedTimes .Valid )
1602+ assert .Equal (t , int64 (1 ), rotatedData .UsedTimes .Int )
1603+
1604+ // Verify the expiry is updated and is in the future
1605+ assert .True (t , rotatedData .ExpiresAt .Before (initialExpiry ), "Expiry should be updated to be sooner than original" )
1606+ assert .True (t , rotatedData .ExpiresAt .After (time .Now ().UTC ()), "Expiry should still be in the future" )
1607+ assert .True (t , time .Until (rotatedData .ExpiresAt ) < time .Until (initialExpiry ), "New expiry should be closer to now than original expiry" )
1608+
1609+ t .Logf ("Original expiry: %v, Updated expiry: %v, Now: %v" , initialExpiry , rotatedData .ExpiresAt , time .Now ())
1610+ })
1611+ }
1612+ }
1613+
1614+ func testHelperAuthorizeCodeInvalidation (x oauth2.InternalRegistry ) func (t * testing.T ) {
1615+ return func (t * testing.T ) {
1616+ ctx := context .Background ()
1617+
1618+ // Create client
1619+ cl := & client.Client {ID : "auth-code-client" }
1620+ require .NoError (t , x .ClientManager ().CreateClient (ctx , cl ))
1621+
1622+ // Create a request with a long expiry
1623+ initialRequest := fosite.Request {
1624+ ID : uuid .New (),
1625+ RequestedAt : time .Now ().UTC ().Round (time .Second ),
1626+ Client : cl ,
1627+ Session : oauth2 .NewSession ("sub" ),
1628+ }
1629+
1630+ // Set a long expiry time (1 hour)
1631+ initialExpiry := time .Now ().Add (1 * time .Hour )
1632+ initialRequest .Session .SetExpiresAt (fosite .AuthorizeCode , initialExpiry )
1633+
1634+ // Create authorize code session
1635+ authCodeSignature := uuid .New ()
1636+ require .NoError (t , x .OAuth2Storage ().CreateAuthorizeCodeSession (ctx , authCodeSignature , & initialRequest ))
1637+
1638+ // Verify initial state
1639+ originalCode , err := x .OAuth2Storage ().GetAuthorizeCodeSession (ctx , authCodeSignature , oauth2 .NewSession ("sub" ))
1640+ require .NoError (t , err )
1641+ require .Equal (t , initialExpiry .Unix (), originalCode .GetSession ().GetExpiresAt (fosite .AuthorizeCode ).Unix ())
1642+
1643+ // Check database directly
1644+ var codeData struct {
1645+ ExpiresAt time.Time `db:"expires_at"`
1646+ Active bool `db:"active"`
1647+ }
1648+ require .NoError (t , x .Persister ().Connection (ctx ).RawQuery (
1649+ "SELECT expires_at, active FROM hydra_oauth2_code WHERE signature=?" ,
1650+ authCodeSignature ).First (& codeData ))
1651+ require .Equal (t , initialExpiry .UTC ().Round (time .Second ), codeData .ExpiresAt .UTC ().Round (time .Second ))
1652+ require .True (t , codeData .Active )
1653+
1654+ // Invalidate the code
1655+ err = x .OAuth2Storage ().InvalidateAuthorizeCodeSession (ctx , authCodeSignature )
1656+ require .NoError (t , err )
1657+
1658+ // Check that the code was invalidated but is still retrievable
1659+ invalidatedCode , err := x .OAuth2Storage ().GetAuthorizeCodeSession (ctx , authCodeSignature , oauth2 .NewSession ("sub" ))
1660+ require .Error (t , err )
1661+ require .ErrorIs (t , err , fosite .ErrInvalidatedAuthorizeCode )
1662+ require .NotNil (t , invalidatedCode ) // Should still be retrievable
1663+
1664+ // Verify database state after invalidation
1665+ var invalidatedData struct {
1666+ ExpiresAt time.Time `db:"expires_at"`
1667+ Active bool `db:"active"`
1668+ }
1669+ require .NoError (t , x .Persister ().Connection (ctx ).RawQuery (
1670+ "SELECT expires_at, active FROM hydra_oauth2_code WHERE signature=?" ,
1671+ authCodeSignature ).First (& invalidatedData ))
1672+
1673+ // Verify the code is now inactive
1674+ require .False (t , invalidatedData .Active )
1675+
1676+ // Verify the expiry is updated to be closer to now than the original expiry
1677+ require .True (t , invalidatedData .ExpiresAt .Before (initialExpiry ),
1678+ "Expiry should be updated to be sooner than original" )
1679+ require .True (t , invalidatedData .ExpiresAt .After (time .Now ()),
1680+ "Expiry should still be in the future" )
1681+ require .True (t , time .Until (invalidatedData .ExpiresAt ) < time .Until (initialExpiry ),
1682+ "New expiry should be closer to now than original expiry" )
1683+
1684+ t .Logf ("Original expiry: %v, Updated expiry: %v, Now: %v" ,
1685+ initialExpiry , invalidatedData .ExpiresAt , time .Now ())
1686+ }
1687+ }
0 commit comments