Употреблять WebAPI2-сайт от клиента Android с помощью Google Authentication

За последние два дня я пытался разглядеть свой мозг, чтобы попытаться понять, как использовать аутентификацию, встроенную в WebAPI 2 ASP.NET, используя Google в качестве внешней аутентификации, и не знакомы с OAuth 2, я совершенно потерян. Я последовал этому руководству, чтобы настроить кнопку входа на моем Android-клиенте и отправить «idToken» в веб-API. Я также следил за этим (теперь устаревшим) учебным пособием по настройке Google в качестве внешнего входа.

Проблема возникает, когда я пытаюсь отправить его. Я получаю {"error":"unsupported_grant_type"} в качестве ответа. Некоторые другие руководства приводят меня к тому, что POST для mysite.com/token не содержит правильных данных. Это означает, что я либо создаю запрос некорректно с клиентом, я как-то неправильно его обрабатываю на бэкэнд, я отправляю его на неправильный URL-адрес, или я делаю что-то совсем другое.

Я нашел этот ответ SO, который говорит, чтобы получить URL-адрес из / api / Accounts / ExternalLogins, но кнопка входа уже дает мне токен доступа, который будет поставляться мне (если я это правильно понимаю).

Если кто-то может помочь мне здесь, на каком точном процессе должен быть от начала до конца, это было бы потрясающе.

ОБНОВЛЕНИЕ: Ладно, вот некоторые вещи, которые я узнал, так как я задал этот вопрос.

  1. Website.com/token URI – это перенаправление для встроенного сервера OAuth в шаблоне WebAPI2. Это не полезно для этой конкретной проблемы.

  2. Id_token – это кодированный токен JWT .

  3. URL-адрес website.com/signin-google – это перенаправление для обычного входа в систему Google, но не принимает эти токены.

  4. Возможно, мне придется написать свой собственный AuthenticationFilter, который использует библиотеку Google Client для авторизации через API Google.

ОБНОВЛЕНИЕ 2: Я все еще работаю над получением этой реализации аутентификации. На данный момент все выглядит хорошо, но я зацикливаюсь на некоторых вещах. Я использовал этот пример, чтобы получить код проверки маркера, и этот учебник, чтобы получить код AuthenticationFilter. Результатом является их сочетание. Я отправлю его здесь как ответ, когда он будет завершен.

Вот мои текущие проблемы:

  1. Создание IPrincipal в качестве вывода. Пример проверки делает ClaimPrincipal, но в примере кода AuthenticationFilter используется UserManager для сопоставления имени пользователя с существующим пользователем и возвращает этого принципала. Приложение ClaimsPrincipal, созданное в примере проверки напрямую, не авто-ассоциируется с существующим пользователем, поэтому мне нужно попытаться сопоставить некоторый элемент претензий с существующим пользователем. Итак, как мне это сделать?

  2. У меня все еще есть неполное представление о том, какой правильный поток для этого. В настоящее время я использую заголовок Authentication для передачи строки id_token с использованием пользовательской схемы: «goog_id_token». Клиент должен отправить свой id_token для каждого метода, вызванного API, с помощью этого настраиваемого AuthenticationFilter. Я понятия не имею, как это обычно делается в профессиональной среде. Похоже, что достаточно распространенный случай использования, что будет много информации об этом, но я этого не видел. Я видел нормальный поток OAuth2, и поскольку я использую только токен идентификатора, а не токен доступа, я немного потерял то, на что должен использоваться токен идентификатора, где он попадает в поток, и Где он должен жить в HTTP-пакете. И потому, что я не знал этого, я как бы делаю это, когда иду.

Вау, я сделал это. Я понял. Я … я не могу в это поверить.

Как уже упоминалось в моем обновленном вопросе «Обновление 2», этот код собран из официального примера API C # Google и учебного примера и примера кода Microsoft Custom AuthenticationFilter. Я собираюсь вставить AuthorizeAsync () здесь и перейти к тому, что делает каждый блок кода. Если вы думаете, что видите проблему, не стесняйтесь говорить об этом.

 public async Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken) { bool token_valid = false; HttpRequestMessage request = context.Request; // 1. Look for credentials in the request //Trace.TraceInformation(request.ToString()); string idToken = request.Headers.Authorization.Parameter.ToString(); 

Клиент добавляет поле заголовка авторизации со схемой, за которой следует одно место, за которым следует токен идентификатора. Это похоже на Authorization: id-token-goog IaMS0m3.Tok3nteXt... Ввод маркера идентификатора в тело, как указано в документации Google, не имеет смысла в этом фильтре, поэтому я решил поместить его в заголовок. По какой-то причине было трудно вытащить пользовательские заголовки из пакетов HTTP, поэтому я просто решил использовать заголовок авторизации с пользовательской схемой, за которой следует токен идентификатора.

  // 2. If there are no credentials, do nothing. if (idToken == null) { Trace.TraceInformation("No credentials."); return; } // 3. If there are credentials, but the filter does not recognize // the authentication scheme, do nothing. if (request.Headers.Authorization.Scheme != "id-token-goog") // Replace this with a more succinct Scheme title. { Trace.TraceInformation("Bad scheme."); return; } 

Вся эта точка фильтра – игнорировать запросы, которые фильтр не регулирует (незнакомые схемы аутентификации и т. Д.), И судить о запросах, которые он должен управлять. Разрешить действительную проверку подлинности для перехода к нисходящему AuthorizeFilter или непосредственно к контроллеру.

Я составил схему «id-token-goog», потому что я понятия не имел, существует ли существующая схема для этого варианта использования. Если есть, кто-нибудь, пожалуйста, дайте мне знать, и я это исправлю. Я думаю, это не имеет особого значения на данный момент, пока мои клиенты все знают схему.

  // 4. If there are credentials that the filter understands, try to validate them. if (idToken != null) { JwtSecurityToken token = new JwtSecurityToken(idToken); JwtSecurityTokenHandler jsth = new JwtSecurityTokenHandler(); // Configure validation Byte[][] certBytes = getCertBytes(); Dictionary<String, X509Certificate2> certificates = new Dictionary<String, X509Certificate2>(); for (int i = 0; i < certBytes.Length; i++) { X509Certificate2 certificate = new X509Certificate2(certBytes[i]); certificates.Add(certificate.Thumbprint, certificate); } { // Set up token validation TokenValidationParameters tvp = new TokenValidationParameters() { ValidateActor = false, // check the profile ID ValidateAudience = (CLIENT_ID != ConfigurationManager .AppSettings["GoogClientID"]), // check the client ID ValidAudience = CLIENT_ID, ValidateIssuer = true, // check token came from Google ValidIssuer = "accounts.google.com", ValidateIssuerSigningKey = true, RequireSignedTokens = true, CertificateValidator = X509CertificateValidator.None, IssuerSigningKeyResolver = (s, securityToken, identifier, parameters) => { return identifier.Select(x => { // TODO: Consider returning null here if you have case sensitive JWTs. /*if (!certificates.ContainsKey(x.Id)) { return new X509SecurityKey(certificates[x.Id]); }*/ if (certificates.ContainsKey(x.Id.ToUpper())) { return new X509SecurityKey(certificates[x.Id.ToUpper()]); } return null; }).First(x => x != null); }, ValidateLifetime = true, RequireExpirationTime = true, ClockSkew = TimeSpan.FromHours(13) }; 

Все это не изменилось из примера Google. Я почти не знаю, что он делает. В основном это создает некоторую магию при создании JWTSecurityToken, анализируемой, декодированной версии строки токенов и устанавливает параметры проверки. Я не уверен, почему нижняя часть этого раздела находится в его собственном блоке оператора, но это имеет какое-то отношение к CLIENT_ID и этому сравнению. Я не уверен, когда и почему значение CLIENT_ID будет когда-либо изменяться, но, по-видимому, это необходимо …

  try { // Validate using the provider SecurityToken validatedToken; ClaimsPrincipal cp = jsth.ValidateToken(idToken, tvp, out validatedToken); if (cp != null) { cancellationToken.ThrowIfCancellationRequested(); ApplicationUserManager um = context .Request .GetOwinContext() .GetUserManager<ApplicationUserManager>(); 

Получить пользовательский менеджер из контекста OWIN. Я должен был копаться в context intellisense, пока не нашел GetOwinCOntext() , а затем обнаружил, что мне нужно добавить using Microsoft.Aspnet.Identity.Owin; Для добавления частичного класса, включающего метод GetUserManager<>() .

  ApplicationUser au = await um .FindAsync( new UserLoginInfo( "Google", token.Subject) ); 

Это было самое последнее, что я должен был исправить. Опять же, мне пришлось прорыть um Intellisense, чтобы найти все функции Find и их переопределения. Я заметил из созданных в моей базе данных таблиц Identity Framework, что есть один пользователь UserLogin, чьи строки содержат поставщика, ключ поставщика и пользователя FK. FindAsync() принимает объект UserLoginInfo , который содержит только строку поставщика и ключ поставщика. У меня была догадка, что эти две вещи теперь связаны. Я также напомнил, что в формате токена есть поле, содержащее ключевое поле, которое было длинным числом, которое начиналось с символа 1.

validatedToken кажется пустым, а не null, но пустой SecurityToken. Вот почему я использую token вместо validatedToken . Я думаю, что с этим что-то не так, но поскольку cp не является нулевым, что является допустимой проверкой на неудачную проверку, он имеет достаточный смысл, что исходный токен действителен.

  // If there is no user with those credentials, return if (au == null) { return; } ClaimsIdentity identity = await um .ClaimsIdentityFactory .CreateAsync(um, au, "Google"); context.Principal = new ClaimsPrincipal(identity); token_valid = true; 

Здесь я должен создать новый ClaimsPrincipal, поскольку созданный выше в валидации пуст (видимо, это правильно). CreateAsync() должен быть третий параметр CreateAsync() . Кажется, это так.

  } } catch (Exception e) { // Multiple certificates are tested. if (token_valid != true) { Trace.TraceInformation("Invalid ID Token."); context.ErrorResult = new AuthenticationFailureResult( "Invalid ID Token.", request); } if (e.Message.IndexOf("The token is expired") > 0) { // TODO: Check current time in the exception for clock skew. Trace.TraceInformation("The token is expired."); context.ErrorResult = new AuthenticationFailureResult( "Token is expired.", request); } Trace.TraceError("Error occurred: " + e.ToString()); } } } } 

Остальное – это просто исключение.

Спасибо, что проверили это. Надеюсь, вы можете посмотреть мои источники и посмотреть, какие компоненты пришли из этой кодовой базы.