Azure-Samples / ms-identity-java-webapp

A Java web application calling Microsoft graph that is secured using the Microsoft identity platform
MIT License
118 stars 105 forks source link

Storing refresh token #68

Closed black-snow closed 3 years ago

black-snow commented 3 years ago

I managed to implement the authentication flow (while still waiting for https://github.com/microsoftgraph/msgraph-sdk-java-auth/issues/14) and now I wonder what I should store long-term so that the users do not have to re-authenticate once their session is gone. The refresh token alone is not enough, I guess, we also need the account, the scopes are good to remember too just like the expiry.

The TokenCache is a candidate (why doesn't ITokenCache implement Serializable?) even though I'd prefer not to store some serialized 3rd party object in the DB that might change at any time. But the silent flow sample also uses the IAuthenticationResult that's held in the session. I might extract the account from the TokenCache but IConfidentialClientApplication.tokenCache() only returns a ITokenCache and not the TokenCache so I'd have to blindly cast it.

tl;dr Before I stitch together a whacky solution - is there an official thing to store away? I'd like to actually use the refresh tokens long-term to not annoy the users with re-authentication. The most reliable and straight forward thing that comes to my mind without digging too deep is to code custom wrappers for TokenCache and IAuthenticationResult to store them away.

Cheers

Avery-Dunn commented 3 years ago

Hello @black-snow : Have a look at this doc for the basic/example way of persisting the token cache. What you describe is similar to what we recommend, except that you implement the ITokenCacheAccessAspect interface and do your serialization/deserialization logic in there. The device code flow sample does this in a pretty simple way, but it should give you an idea of what's intended.

Also, depending on how you want to store the cache we have a small library of extensions which has some persistence features that you might find useful, mainly focused on persisting the cache securely.

black-snow commented 3 years ago

@Avery-Dunn thanks for the quick reply!

How do I get the account to build the SilentParameters? Do I have to store it away separately? Why does TokenCache/ ITokenCache not give access to its fields - why can't I just call TokenCache.getAccounts()? :|

I also can't wrap TokenCache since most stuff inside of it is package private. I could have my own Jackson-annotated POJO and deserialize the token cache into this one ... no wait, I can't, AccountCacheEntity is also package private.

/edit: For now I tackled the issue by providing my own IAccount and extracting homeAccountId, environment and username from the serialized token cache. But that's of course a shitty hack.

Avery-Dunn commented 3 years ago

@black-snow For the accounts, it depends on how you're using the cache

For the token cache, most of this was all designed before my time here, but I believe the reason that most of the internals of the token cache aren't public is because they shouldn't ever need to be adjusted manually, refreshing is taken care of by the library's silent calls, and I think pretty much everything in the token cache is retrievable in some way:

black-snow commented 3 years ago

@Avery-Dunn thanks for the thorough reply! I missed the other constructor on SilentParameters. Now it's down to using my own ITokenCacheAccessAspect.

I'll open some issues in the msal4j repo.

Thanks again and have some nice holidays!

black-snow commented 3 years ago

@Avery-Dunn FYI

and if not provided an account then it will still work if each cache has one account

That doesn't seem to be true.

final SilentParameters parameters = SilentParameters.builder(
    Collections.singleton("User.Read")
).build();
app.acquireTokenSilently(parameters);

This results in a warning, that the token cache could not be found (although it is there). I didn't investigate much, but looking at https://github.com/AzureAD/microsoft-authentication-library-for-java/blob/b447f9e1f1fd8026208f2410b095e696f13398b2/src/main/java/com/microsoft/aad/msal4j/AcquireTokenSilentSupplier.java#L33 there's a lot of shenanigans when an account is set vs. when no account is given.

I now pull the account from the app:

Set<IAccount> accounts = app.getAccounts().get(1L, TimeUnit.SECONDS);
account = accounts.isEmpty() ? null :  accounts.iterator().next();
final SilentParameters parameters = SilentParameters.builder(
    Collections.singleton("User.Read"),
    account
).build();
app.acquireTokenSilently(parameters);

and it works fine with the stored refresh tokens.