Статьи

Совместное использование токенов доступа единого входа Azure Active Directory в нескольких собственных мобильных приложениях

Этот пост является четвертым и последним в серии, посвященной проверке подлинности единого входа Azure Active Directory (SSO) в собственных мобильных приложениях.

  1. Аутентификация пользователей приложений iOS с помощью Azure Active Directory
  2. Как лучше всего обрабатывать токены доступа AAD в нативных мобильных приложениях
  3. Использование токенов единого входа Azure для нескольких ресурсов AAD из собственных мобильных приложений
  4. Совместное использование токенов доступа Azure SSO для нескольких собственных мобильных приложений. (Этот пост)

Вступление

На большинстве предприятий имеется более одного мобильного приложения, и эти мобильные приложения нередко взаимодействуют с некоторыми внутренними службами или API-интерфейсами для получения и обновления данных. В предыдущих статьях этой серии мы рассмотрели, как управлять доступом к API и обмениваться токенами как способом, позволяющим мобильному приложению взаимодействовать с несколькими ресурсами, защищенными AAD.

В этом посте будет рассказано, как делиться токенами доступа к ресурсам AAD в нескольких мобильных приложениях . Это очень полезно, если предприятие (или любой поставщик мобильных приложений) хочет предоставить пользователям удобство, не запрашивая у них аутентификацию в каждом мобильном приложении, которое есть у пользователя.

Когда мобильный пользователь входит в Azure AD и получает токен, мы хотим повторно использовать этот токен с другими приложениями. Это подходит для некоторых сценариев, но не может быть идеальным для приложений и ресурсов, которые имеют дело с конфиденциальными данными — решение за вами.

Совместное использование токенов в нескольких мобильных приложениях

Теперь перейдем к предыдущим сообщениям, и теперь пришло время предоставить нашим мобильным приложениям доступ к токену доступа. В этом сценарии мы рассматриваем устройства iOS (iOS 6 и выше), однако другие мобильные платформы также предоставляют аналогичные возможности. Итак, какие есть варианты для обмена данными на iOS:

Магазин ключей

iOS предлагает разработчикам простую утилиту для хранения и обмена ключами, которая называется SecKeyChain. API был частью платформы iOS еще до iOS 6, но в iOS 6 Apple интегрировала этот инструмент с iCloud, чтобы упростить передачу любых сохраненных паролей и ключей в Apple iCloud, а затем делиться ими на нескольких устройствах.

Мы могли бы использовать iOS SecKeyChain для хранения токена (и refreshToken), когда пользователь входит в любое из приложений. Когда пользователь начинает использовать любое из других приложений, мы сначала проверяем SecKeyChain, прежде чем пытаться аутентифицировать пользователя.

public async Task AsyncInit(UIViewController controller, ITokensRepository repository)
{
    _controller = controller;
    _repository = repository;
    _authContext = new AuthenticationContext(authority);
}
 
public async Task<string> RefreshTokensLocally()
{
    var refreshToken = _repository.GetKey(Constants.CacheKeys.RefreshToken, string.Empty);
    var authorizationParameters = new AuthorizationParameters(_controller);
 
    var result = "Refreshed an existing Token";
    bool hasARefreshToken = true;
 
    if (string.IsNullOrEmpty(refreshToken))
    {
        var localAuthResult = await _authContext.AcquireTokenAsync(
            resourceId1, clientId,
                        new Uri (redirectUrl),
                        authorizationParameters,
                         UserIdentifier.AnyUser, null);
 
        refreshToken = localAuthResult.RefreshToken;
        _repository.SaveKey(Constants.CacheKeys.WebService1Token, localAuthResult.AccessToken, null);
 
 
        hasARefreshToken = false;
        result = "Acquired a new Token";
    }
 
    var refreshAuthResult = await _authContext.AcquireTokenByRefreshTokenAsync(refreshToken, clientId, resourceId2);
    _repository.SaveKey(Constants.CacheKeys.WebService2Token, refreshAuthResult.AccessToken, null);
 
    if (hasARefreshToken)
    {
        // this will only be called when we try refreshing the tokens (not when we are acquiring new tokens.
        refreshAuthResult = await _authContext.AcquireTokenByRefreshTokenAsync(refreshAuthResult.RefreshToken,  clientId,  resourceId1);
        _repository.SaveKey(Constants.CacheKeys.WebService1Token, refreshAuthResult.AccessToken, null);
    }
 
    _repository.SaveKey(Constants.CacheKeys.RefreshToken, refreshAuthResult.RefreshToken, null);
 
    return result;
}

Some of the above code will be familiar from previous posts, but what has changed is that now we are passing ITokenRepository which would save any tokens (and refreshTokens) once the user logs in to make them available for other mobile apps.

I have intentionally passed an interface (ITokenRepository) to allow for different implementations, in case you opt to use a different approach for sharing the tokens. The internal implementation of the concrete TokenRepository is something like this:

public interface ITokensRepository
{
    bool SaveKey(string key, string val, string keyDescription);
    string GetKey(string key, string defaultValue);
    bool SaveKeys(Dictionary<string,string> secrets);
}
 
public class TokensRepository : ITokensRepository
{
    private const string _keyChainAccountName = "myService";
 
    public bool SaveKey(string key, string val, string keyDescription)
    {
        var setResult = KeychainHelpers.SetPasswordForUsername(key, val, _keyChainAccountName, SecAccessible.WhenUnlockedThisDeviceOnly, false );
 
        return setResult == SecStatusCode.Success;
    }
 
    public string GetKey(string key, string defaultValue)
    {
        return KeychainHelpers.GetPasswordForUsername(key, _keyChainAccountName, false) ?? defaultValue;
    }
         
    public bool SaveKeys(Dictionary<string,string> secrets)
    {
        var result = true;
        foreach (var key in secrets.Keys)
        {
            result = result && SaveKey(key, secrets [key], string.Empty);
        }
 
        return result;
    }
}

iCloud

We could use Apple iCloud to push the access tokens to the cloud and share them with other apps. The approach would be similar to what we have done above with the only difference being in the way we are storing these keys. Instead of storing them locally, we push them to Apple iCloud directly. As the SecKeyChain implementation above does support pushing data to iCloud, I won’t go through the implementation details here and simply note the option is available for you.

Third Party Cloud Providers (ie Azure)

Similar to the previous option, but offer more flexibility. This is a very good solution if we already are already using Azure Mobile Services for our mobile app. We can create one more table and then use this table to store and share access tokens. The implementation of this could be similar to the following:

public async Task<string> RefreshTokensInAzureTable()
{
    var tokensListOnAzure = await tokensTable.ToListAsync();
    var tokenEntry = tokensListOnAzure.FirstOrDefault();
    var authorizationParameters = new AuthorizationParameters(_controller);
 
    var result = "Refreshed an existing Token";
    bool hasARefreshToken = true;
 
    if (tokenEntry == null)
    {
        var localAuthResult = await _authContext.AcquireTokenAsync(resourceId1, clientId, new Uri (redirectUrl),  authorizationParameters, UserIdentifier.AnyUser, null);
 
        tokenEntry = new Tokens {
            WebApi1AccessToken = localAuthResult.AccessToken,
            RefreshToken = localAuthResult.RefreshToken,
            Email = localAuthResult.UserInfo.DisplayableId,
            ExpiresOn = localAuthResult.ExpiresOn
        };
        hasARefreshToken = false;
        result = "Acquired a new Token";
    }
         
    var refreshAuthResult = await _authContext.AcquireTokenByRefreshTokenAsync(tokenEntry.RefreshToken,
                                                                                    clientId,
                                                                                    resourceId2);
    tokenEntry.WebApi2AccessToken = refreshAuthResult.AccessToken;
    tokenEntry.RefreshToken = refreshAuthResult.RefreshToken;
    tokenEntry.ExpiresOn = refreshAuthResult.ExpiresOn;
 
    if (hasARefreshToken)
    {
        // this will only be called when we try refreshing the tokens (not when we are acquiring new tokens.
        refreshAuthResult = await _authContext.AcquireTokenByRefreshTokenAsync (refreshAuthResult.RefreshToken, clientId, resourceId1);
        tokenEntry.WebApi1AccessToken = refreshAuthResult.AccessToken;
        tokenEntry.RefreshToken = refreshAuthResult.RefreshToken;
        tokenEntry.ExpiresOn = refreshAuthResult.ExpiresOn;
    }
 
    if (hasARefreshToken)
        await tokensTable.UpdateAsync (tokenEntry);
    else
        await tokensTable.InsertAsync (tokenEntry);
 
    return result;
}

Other Local Means

There are many other options that are available for storing and sharing data between apps on iOS. We could create a reminder, a contact, etc. Many of these entries could even be hidden from the user. For instance, “DONE” reminders are hidden from the Reminders app list. So we would need to to worry about the keys visibility, but I would still warn you to thing 10 times before go into storing your keys this way.

Words of Warning

Bearer Tokens

Developers need to understand Bearer Tokens when using Azure AD authentication. Bearer Tokens mean anybody who has the token (bearer of the token) could access and interact with your AAD resource. This offers high flexibility but it could also be a security risk if your key was exposed somehow. This needs to be thought of when implementing any token sharing mechanism.

iOS SecKeyChain is “Secure”

iOS SecKeyChain is “Secure”, right? No, not at all. Apple calls it secure, but on jail-broken devices, you could see the key store as a normal file. Thus, I would highly recommend encrypting these access tokens and any key that you might want to store before persisting it. The same goes for iCloud, Azure, or any of the other approaches we went through above.

Apple AppStore Verification

If you intend on submitting your app to Apple AppStore, then you need to be extra careful with what approach you take to share data between your apps. For enterprises (locally deployed apps), you have the control and you make the call based on your use case. However, Apple has a history of rejecting apps (ie PastePane) for using some of iOS APIs in “an unintended” manner.

I hope you found this series of posts useful, and as usual, if there is something not clear or you need some help with similar projects that you are undertaking, then get in touch, and we will do our best to help. I have pushed the sample code from this post and the previous ones to GitHub, and can be found here

This blog post is the forth and final in the series that cover Azure Active Directory Single Sign-On (SSO) authentication in native mobile applications.