In order to invalidate a user token you need to do things :
Call 'invalidate refresh Token' custom action on the user. The call to this endpoint updates the "refreshTokensValidFromDateTime" property to dateTimeNow(UTC)
Update your custom policies to look at this field when the refresh token is used to get a new access/idToken
These changes checks if the "refreshTokensValidFromDateTime" is newer than the refreshToken issued tim. If it is then it means that the old refresh token has been invalidated.
To invalidate a refresh token for a user you need to call this endpoint in graph
https://graph.windows.net/contoso.onmicrosoft.com/users/3ff8bee2-d1dc-445a-bca1-64101d478f43/invalidateAllRefreshTokens?api-version=1.6
Clock Skew
There is a clock skew to account for the potential difference in observed time between the server that created the refresh token (Azure AD B2C service) and the server that stamps the refreshTokenValidFromDateTime value on the user object (the Graph service). It is currently set to 300000 milliseconds (or 5 minutes)
To account for Clock Skew you can update the ClaimsTransformtion = AssertRefreshTokenIssuedLaterThanValidFromDate
with this value
<InputParameter Id="TreatAsEqualIfWithinMillseconds" DataType="int" Value="10000" />
Refresh Token invalidation can be called adhoc by the app or you can write an API in your password reset journey that calls graph, or call it on demand when required. The user that goes through password reset journey in that case will see their session being ended and the user will be prompted on all other devices.
Changes Needed
For custom policies the following Technical Profile (TP), Claims Transformation (CT), and Claims Provider (CP) needs to be added in order for the policy to check if the refresh token was invalidated.
<ClaimsTransformation Id="AssertRefreshTokenIssuedLaterThanValidFromDate" TransformationMethod="AssertDateTimeIsGreaterThan">
<InputClaims>
<InputClaim ClaimTypeReferenceId="refreshTokenIssuedOnDateTime" TransformationClaimType="leftOperand" />
<InputClaim ClaimTypeReferenceId="refreshTokensValidFromDateTime" TransformationClaimType="rightOperand" />
</InputClaims>
<InputParameters>
<InputParameter Id="AssertIfEqualTo" DataType="boolean" Value="false" />
<InputParameter Id="AssertIfRightOperandIsNotPresent" DataType="boolean" Value="true" />
<InputParameter Id="TreatAsEqualIfWithinMillseconds" DataType="int" Value="10000" />
</InputParameters>
</ClaimsTransformation>
...
<ClaimsProvider>
<DisplayName>Token Issuer</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="JwtIssuer">
<DisplayName>JWT Issuer</DisplayName>
<Protocol Name="None" />
<OutputTokenFormat>JWT</OutputTokenFormat>
<Metadata>
<Item Key="client_id">{service:te}</Item>
<Item Key="issuer_refresh_token_user_identity_claim_type">objectId</Item>
<Item Key="SendTokenResponseBodyWithJsonNumbers">true</Item>
<Item Key="RefreshTokenUserJourneyId">RedeemRefreshTokenV1</Item>
</Metadata>
<CryptographicKeys>
<Key Id="issuer_secret" StorageReferenceId="B2C_1A_TokenSigningKeyContainer" />
<Key Id="issuer_refresh_token_key" StorageReferenceId="B2C_1A_TokenEncryptionKeyContainer" />
</CryptographicKeys>
<InputClaims />
<OutputClaims />
</TechnicalProfile>
...
<TechnicalProfile Id="TpEngine_RefreshTokenReadAndSetup">
<DisplayName>Trustframework Policy Engine Refresh Token Setup Technical Profile</DisplayName>
<Protocol Name="None" />
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="objectId" />
<OutputClaim ClaimTypeReferenceId="refreshTokenIssuedOnDateTime" />
</OutputClaims>
</TechnicalProfile>
</TechnicalProfiles>
...
<TechnicalProfile Id="AAD-UserReadUsingObjectId-CheckRefreshTokenDate">
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="refreshTokensValidFromDateTime" />
</OutputClaims>
<OutputClaimsTransformations>
<OutputClaimsTransformation ReferenceId="AssertRefreshTokenIssuedLaterThanValidFromDate" />
</OutputClaimsTransformations>
<IncludeTechnicalProfile ReferenceId="AAD-UserReadUsingObjectId" />
</TechnicalProfile>
...
<UserJourney Id="RedeemRefreshTokenV1">
<AssuranceLevel>LOA1</AssuranceLevel>
<PreserveOriginalAssertion>false</PreserveOriginalAssertion>
<OrchestrationSteps>
<OrchestrationStep Order="1" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="RefreshTokenSetupExchange" TechnicalProfileReferenceId="TpEngine_RefreshTokenReadAndSetup" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="2" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="CheckRefreshTokenDateFromAadExchange" TechnicalProfileReferenceId="AAD-UserReadUsingObjectId-CheckRefreshTokenDate" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="3" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer" />
</OrchestrationSteps>
</UserJourney>
This journey will get called when using the Refresh Token, and actually check for the refreshTokenLastValidFrom timestamp that was updated in the Graph API call.