VS에서 검색 할 때 나타나는 기본 패키지는 새로 고침 토큰을 가져 오는 것을 지원하지 않는 'Xamarin Auth 1.3.0'입니다.
내 자신의 버전을 OAuth2Authenticator
으로 만들었으며 매우 제한된 노력으로 같은 목표에 도달했습니다. 안전하게 저장해야 새로 고침 토큰과 AccountStore에
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();
이 호출의 결과를 다음과 같이 LoginPageRenderer
이에서
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);
}
}
}
가 호출됩니다.
나는 새로운 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은 설치된 자격 증명 사용을 지원하지 않기 때문에 현재로서는이 문제를 해결할 방법이 없습니다.