使用xamarin.auth获取google api的刷新令牌

时间:2016-07-07 15:05:45

标签: xamarin oauth google-api xamarin.auth

 var auth = new OAuth2Authenticator(clientId: "my client id",
        scope: "https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/calendar",
        authorizeUrl: new Uri("https://accounts.google.com/o/oauth2/auth"),
        redirectUrl: new Uri("https://www.googleapis.com/plus/v1/people/me"),
        getUsernameAsync: null);
        auth.AllowCancel = allowCancel;
        List<Event> events = new List<Event>();

        auth.Completed += async (sender, e) =>
         {
             if (!e.IsAuthenticated)
             {
                 Toast.MakeText(_activity, "Fail to authenticate!", ToastLength.Short).Show();
                 return;
             }

             string access_token;
             e.Account.Properties.TryGetValue("access_token", out access_token);

当我尝试以相同的方式检索刷新令牌时,我检索访问令牌它不起作用。

我能够获得访问令牌但无法获得刷新令牌。我尝试从谷歌帐户删除授权,因为我读到你第一次授予访问权限时只获得刷新令牌,但我仍然无法检索访问令牌。我还读过我需要在请求中添加access_type = offline和approval_prompt = force,但我不知道在哪里使用xamarin.auth添加它。

1 个答案:

答案 0 :(得分:0)

在VS中搜索时获得的默认包是&#39; Xamarin Auth 1.3.0&#39;它不支持获取刷新令牌。

我已经通过制作我自己的OAuth2Authenticator版本来解决这个问题,并且付出了非常有限的努力达到了同样的目标。

public class RefreshOAuth2Authenticator : WebRedirectAuthenticator

{
    string clientId;
    string clientSecret;
    string scope;
    Uri authorizeUrl;
    Uri accessTokenUrl;
    Uri redirectUrl;
    GetUsernameAsyncFunc getUsernameAsync;

    string requestState;
    bool reportedForgery;

    public string ClientId
    {
        get { return clientId; }
    }

    public string ClientSecret
    {
        get { return clientSecret; }
    }

    public string Scope
    {
        get { return scope; }
    }

    public Uri AuthorizeUrl
    {
        get { return authorizeUrl; }
    }

    public Uri RedirectUrl
    {
        get { return redirectUrl; }
    }

    public Uri AccessTokenUrl
    {
        get { return accessTokenUrl; }
    }

    public RefreshOAuth2Authenticator(string clientId, string scope, Uri authorizeUrl, Uri redirectUrl, GetUsernameAsyncFunc getUsernameAsync = null)
        : this (redirectUrl)
    {
        if (string.IsNullOrEmpty(clientId))
        {
            throw new ArgumentException("clientId must be provided", nameof(clientId));
        }
        this.clientId = clientId;

        this.scope = scope ?? "";

        if (authorizeUrl == null)
        {
            throw new ArgumentNullException(nameof(authorizeUrl));
        }
        this.authorizeUrl = authorizeUrl;

        this.getUsernameAsync = getUsernameAsync;

        this.redirectUrl = redirectUrl;

        accessTokenUrl = null;
    }

    public RefreshOAuth2Authenticator(string clientId, string clientSecret, string scope, Uri authorizeUrl, Uri redirectUrl, Uri accessTokenUrl, GetUsernameAsyncFunc getUsernameAsync = null)
        : this (redirectUrl, clientSecret, accessTokenUrl)
    {
        if (string.IsNullOrEmpty(clientId))
        {
            throw new ArgumentException("clientId must be provided", nameof(clientId));
        }
        this.clientId = clientId;

        if (string.IsNullOrEmpty(clientSecret))
        {
            throw new ArgumentException("clientSecret must be provided", nameof(clientSecret));
        }
        this.clientSecret = clientSecret;

        this.scope = scope ?? "";

        if (authorizeUrl == null)
        {
            throw new ArgumentNullException(nameof(authorizeUrl));
        }
        this.authorizeUrl = authorizeUrl;

        if (accessTokenUrl == null)
        {
            throw new ArgumentNullException(nameof(accessTokenUrl));
        }
        this.accessTokenUrl = accessTokenUrl;

        this.redirectUrl = redirectUrl;

        this.getUsernameAsync = getUsernameAsync;
    }

    RefreshOAuth2Authenticator(Uri redirectUrl, string clientSecret = null, Uri accessTokenUrl = null)
        : base (redirectUrl, redirectUrl)
    {
        this.clientSecret = clientSecret;

        this.accessTokenUrl = accessTokenUrl;

        this.redirectUrl = redirectUrl;

        //
        // Generate a unique state string to check for forgeries
        //
        var chars = new char[16];
        var rand = new Random();
        for (var i = 0; i < chars.Length; i++)
        {
            chars[i] = (char)rand.Next('a', 'z' + 1);
        }
        requestState = new string(chars);
    }

    bool IsImplicit { get { return accessTokenUrl == null; } }

    public override Task<Uri> GetInitialUrlAsync()
    {
        var url = new Uri(string.Format(
            "{0}?client_id={1}&redirect_uri={2}&response_type={3}&scope={4}&state={5}&access_type=offline&prompt=consent",
            authorizeUrl.AbsoluteUri,
            Uri.EscapeDataString(clientId),
            Uri.EscapeDataString(RedirectUrl.AbsoluteUri),
            IsImplicit ? "token" : "code",
            Uri.EscapeDataString(scope),
            Uri.EscapeDataString(requestState)));

        var tcs = new TaskCompletionSource<Uri>();
        tcs.SetResult(url);
        return tcs.Task;
    }

    public async virtual Task<IDictionary<string, string>> RequestRefreshTokenAsync(string refreshToken)
    {
        var queryValues = new Dictionary<string, string>
        {
            {"refresh_token", refreshToken},
            {"client_id", this.ClientId},
            {"grant_type", "refresh_token"}
        };

        if (!string.IsNullOrEmpty(this.ClientSecret))
        {
            queryValues["client_secret"] = this.ClientSecret;
        }

        try
        {
            var accountProperties = await RequestAccessTokenAsync(queryValues).ConfigureAwait(false);

            this.OnRetrievedAccountProperties(accountProperties);

            return accountProperties;
        }
        catch (Exception e)
        {
            OnError(e);

            throw; // maybe don't need this? this will throw the exception in order to maintain backward compatibility, but maybe could just return -1 or something instead?
        }
    }

    protected override void OnPageEncountered(Uri url, IDictionary<string, string> query, IDictionary<string, string> fragment)
    {
        var all = new Dictionary<string, string>(query);
        foreach (var kv in fragment)
            all[kv.Key] = kv.Value;

        //
        // Check for forgeries
        //
        if (all.ContainsKey("state"))
        {
            if (all["state"] != requestState && !reportedForgery)
            {
                reportedForgery = true;
                OnError("Invalid state from server. Possible forgery!");
                return;
            }
        }

        //
        // Continue processing
        //
        base.OnPageEncountered(url, query, fragment);
    }

    protected override void OnRedirectPageLoaded(Uri url, IDictionary<string, string> query, IDictionary<string, string> fragment)
    {
        //
        // Look for the access_token
        //
        if (fragment.ContainsKey("access_token"))
        {
            //
            // We found an access_token
            //
            OnRetrievedAccountProperties(fragment);
        }
        else if (!IsImplicit)
        {
            //
            // Look for the code
            //
            if (query.ContainsKey("code"))
            {
                var code = query["code"];
                RequestAccessTokenAsync(code).ContinueWith(task =>
                {
                    if (task.IsFaulted)
                    {
                        OnError(task.Exception);
                    }
                    else {
                        OnRetrievedAccountProperties(task.Result);
                    }
                }, TaskScheduler.FromCurrentSynchronizationContext());
            }
            else {
                OnError("Expected code in response, but did not receive one.");
                return;
            }
        }
        else {
            OnError("Expected access_token in response, but did not receive one.");
            return;
        }
    }

    Task<IDictionary<string, string>> RequestAccessTokenAsync(string code)
    {
        var queryValues = new Dictionary<string, string> {
            { "grant_type", "authorization_code" },
            { "code", code },
            { "redirect_uri", RedirectUrl.AbsoluteUri },
            { "client_id", clientId },
        };
        if (!string.IsNullOrEmpty(clientSecret))
        {
            queryValues["client_secret"] = clientSecret;
        }

        return RequestAccessTokenAsync(queryValues);
    }

    protected Task<IDictionary<string, string>> RequestAccessTokenAsync(IDictionary<string, string> queryValues)
    {
        var query = queryValues.FormEncode();

        var req = WebRequest.Create(accessTokenUrl);
        req.Method = "POST";
        var body = Encoding.UTF8.GetBytes(query);
        req.ContentLength = body.Length;
        req.ContentType = "application/x-www-form-urlencoded";
        using (var s = req.GetRequestStream())
        {
            s.Write(body, 0, body.Length);
        }
        return req.GetResponseAsync().ContinueWith(task =>
        {
            var text = task.Result.GetResponseText();

            // Parse the response
            var data = text.Contains("{") ? WebEx.JsonDecode(text) : WebEx.FormDecode(text);

            if (data.ContainsKey("error"))
            {
                throw new AuthException("Error authenticating: " + data["error"]);
            }
            else if (data.ContainsKey("access_token"))
            {
                return data;
            }
            else {
                throw new AuthException("Expected access_token in access token response, but did not receive one.");
            }
        });
    }

    protected virtual void OnRetrievedAccountProperties(IDictionary<string, string> accountProperties)
    {
        //
        // Now we just need a username for the account
        //
        if (getUsernameAsync != null)
        {
            getUsernameAsync(accountProperties).ContinueWith(task =>
            {
                if (task.IsFaulted)
                {
                    OnError(task.Exception);
                }
                else {
                    OnSucceeded(task.Result, accountProperties);
                }
            }, TaskScheduler.FromCurrentSynchronizationContext());
        }
        else {
            OnSucceeded("", accountProperties);
        }
    }

}

LoginPageRenderer开始,如下所示:

var auth = new RefreshOAuth2Authenticator(
                clientId: "client ID",
                clientSecret: "client Secret",
                scope: "scope", // just 'openid' in my case
                authorizeUrl: new Uri("https://accounts.google.com/o/oauth2/v2/auth"),
                redirectUrl: new Uri("your redirect url"),
                accessTokenUrl: new Uri("https://www.googleapis.com/oauth2/v4/token") );

auth.GetUI();

此调用会产生一个带有刷新令牌的AccountStore,应该安全地保存。

每次我想获得一个新的id_token,我只需要打电话:

await auth.RequestRefreshTokenAsync("Refresh Token")

你需要更多关于检查expire_ts和存储令牌值的逻辑,但是上面的代码应该让你去。

注1:Xamarin.Auth的分支现在支持刷新令牌 https://github.com/xamarin/Xamarin.Auth/tree/portable-bait-and-switch我没有尝试过。

注意2:我知道客户端密钥存储在应用程序中,出于安全原因,建议不要这样做。但是,由于Google不支持使用已安装的凭据,因此我目前无法看到解决此问题的方法。