ExchangeUserCredentialForToken
関数を使用して、承認サーバーからトークンを取得しています。ユーザーがデータベースに存在する場合は問題なく動作しますが、資格情報が正しくない場合は、クライアントにメッセージを送り返したいと思います。エラーメッセージを設定するために、次の2行のコードを使用しています。
context.SetError("Autorization Error", "The username or password is incorrect!");
context.Rejected();
しかし、クライアント側では、プロトコルエラー(エラー400)しか発生しません。承認サーバーのサーバー側で設定されたエラーメッセージを取得する方法を教えてください。
承認サーバーからの完全なアプリ構成:
using Constants;
using Microsoft.Owin;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.Infrastructure;
using Microsoft.Owin.Security.OAuth;
using Owin;
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Security.Claims;
using System.Security.Principal;
using System.Threading.Tasks;
using AuthorizationServer.Entities;
using AuthorizationServer.Entities.Infrastructure.Abstract;
using AuthorizationServer.Entities.Infrastructure.Concrete;
namespace AuthorizationServer
{
public partial class Startup
{
private IEmployeeRepository Repository;
public void ConfigureAuth(IAppBuilder app)
{
//instanciate the repository
Repository = new EmployeeRepository();
// Enable Application Sign In Cookie
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Application",
AuthenticationMode = AuthenticationMode.Passive,
LoginPath = new PathString(Paths.LoginPath),
LogoutPath = new PathString(Paths.LogoutPath),
});
// Enable External Sign In Cookie
app.SetDefaultSignInAsAuthenticationType("External");
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "External",
AuthenticationMode = AuthenticationMode.Passive,
CookieName = CookieAuthenticationDefaults.CookiePrefix + "External",
ExpireTimeSpan = TimeSpan.FromMinutes(5),
});
// Enable google authentication
app.UseGoogleAuthentication();
// Setup Authorization Server
app.UseOAuthAuthorizationServer(new OAuthAuthorizationServerOptions
{
AuthorizeEndpointPath = new PathString(Paths.AuthorizePath),
TokenEndpointPath = new PathString(Paths.TokenPath),
ApplicationCanDisplayErrors = true,
#if DEBUG
AllowInsecureHttp = true,
#endif
// Authorization server provider which controls the lifecycle of Authorization Server
Provider = new OAuthAuthorizationServerProvider
{
OnValidateClientRedirectUri = ValidateClientRedirectUri,
OnValidateClientAuthentication = ValidateClientAuthentication,
OnGrantResourceOwnerCredentials = GrantResourceOwnerCredentials,
OnGrantClientCredentials = GrantClientCredetails
},
// Authorization code provider which creates and receives authorization code
AuthorizationCodeProvider = new AuthenticationTokenProvider
{
OnCreate = CreateAuthenticationCode,
OnReceive = ReceiveAuthenticationCode,
},
// Refresh token provider which creates and receives referesh token
RefreshTokenProvider = new AuthenticationTokenProvider
{
OnCreate = CreateRefreshToken,
OnReceive = ReceiveRefreshToken,
}
});
// indicate our intent to use bearer authentication
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions
{
AuthenticationType = "Bearer",
AuthenticationMode = Microsoft.Owin.Security.AuthenticationMode.Active
});
}
private Task ValidateClientRedirectUri(OAuthValidateClientRedirectUriContext context)
{
if (context.ClientId == Clients.Client1.Id)
{
context.Validated(Clients.Client1.RedirectUrl);
}
else if (context.ClientId == Clients.Client2.Id)
{
context.Validated(Clients.Client2.RedirectUrl);
}
return Task.FromResult(0);
}
private Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
string clientname;
string clientpassword;
if (context.TryGetBasicCredentials(out clientname, out clientpassword) ||
context.TryGetFormCredentials(out clientname, out clientpassword))
{
employee Employee = Repository.GetEmployee(clientname, clientpassword);
if (Employee != null)
{
context.Validated();
}
else
{
context.SetError("Autorization Error", "The username or password is incorrect!");
context.Rejected();
}
}
return Task.FromResult(0);
}
private Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
var identity = new ClaimsIdentity(new GenericIdentity(context.UserName, OAuthDefaults.AuthenticationType), context.Scope.Select(x => new Claim("urn:oauth:scope", x)));
context.Validated(identity);
return Task.FromResult(0);
}
private Task GrantClientCredetails(OAuthGrantClientCredentialsContext context)
{
var identity = new ClaimsIdentity(new GenericIdentity(context.ClientId, OAuthDefaults.AuthenticationType), context.Scope.Select(x => new Claim("urn:oauth:scope", x)));
context.Validated(identity);
return Task.FromResult(0);
}
private readonly ConcurrentDictionary<string, string> _authenticationCodes =
new ConcurrentDictionary<string, string>(StringComparer.Ordinal);
private void CreateAuthenticationCode(AuthenticationTokenCreateContext context)
{
context.SetToken(Guid.NewGuid().ToString("n") + Guid.NewGuid().ToString("n"));
_authenticationCodes[context.Token] = context.SerializeTicket();
}
private void ReceiveAuthenticationCode(AuthenticationTokenReceiveContext context)
{
string value;
if (_authenticationCodes.TryRemove(context.Token, out value))
{
context.DeserializeTicket(value);
}
}
private void CreateRefreshToken(AuthenticationTokenCreateContext context)
{
context.SetToken(context.SerializeTicket());
}
private void ReceiveRefreshToken(AuthenticationTokenReceiveContext context)
{
context.DeserializeTicket(context.Token);
}
}
}
ここに、私の元の投稿と合わせてJeffの概念を使用した完全なソリューションがあります。
1)コンテキストでのエラーメッセージの設定
エラーメッセージを設定した後でcontext.Rejected()を呼び出すと、エラーメッセージが削除されます(以下の例を参照)。
context.SetError("Account locked",
"You have exceeded the total allowed failed logins. Please try back in an hour.");
context.Rejected();
タスクからcontext.Rejected()を削除する必要があります。 RejectedメソッドとSetErrorメソッドの定義は次のとおりです。
拒否されました:
このコンテキストをアプリケーションによって検証されていないものとしてマークします。 IsValidatedおよびHasErrorは、呼び出しの結果としてfalseになります。
SetError:
このコンテキストをアプリケーションによって検証されていないものとしてマークし、さまざまなエラー情報プロパティを割り当てます。呼び出しの結果、HasErrorがtrueになり、IsValidatedがfalseになります。
ここでも、エラーを設定した後でRejectedメソッドを呼び出すと、コンテキストにエラーがないとマークされ、エラーメッセージが削除されます。
2)応答のステータスコードの設定:Jeffの例を使用して、少しスピンをかけます
マジックストリングを使用する代わりに、ステータスコードのタグを設定するためのグローバルプロパティを作成します。静的グローバルクラスで、ステータスコードにフラグを付けるためのプロパティを作成します(X-Challengeを使用しましたが、もちろん何でも選択できます)。これは、応答に追加されるヘッダープロパティにフラグを付けるために使用されます。
public static class ServerGlobalVariables
{
//Your other properties...
public const string OwinChallengeFlag = "X-Challenge";
}
次に、OAuthAuthorizationServerProviderのさまざまなタスクで、タグをキーとして応答の新しいヘッダー値に追加します。グローバルフラグと組み合わせてHttpStatusCode列挙型を使用すると、さまざまなステータスコードのすべてにアクセスでき、魔法の文字列を回避できます。
//Set the error message
context.SetError("Account locked",
"You have exceeded the total allowed failed logins. Please try back in an hour.");
//Add your flag to the header of the response
context.Response.Headers.Add(ServerGlobalVariables.OwinChallengeFlag,
new[] { ((int)HttpStatusCode.Unauthorized).ToString() });
お客様のOwinMiddlewareでは、グローバル変数を使用してヘッダー内のフラグを検索できます。
//This class handles all the OwinMiddleware responses, so the name should
//not just focus on invalid authentication
public class CustomAuthenticationMiddleware : OwinMiddleware
{
public CustomAuthenticationMiddleware(OwinMiddleware next)
: base(next)
{
}
public override async Task Invoke(IOwinContext context)
{
await Next.Invoke(context);
if (context.Response.StatusCode == 400
&& context.Response.Headers.ContainsKey(
ServerGlobalVariables.OwinChallengeFlag))
{
var headerValues = context.Response.Headers.GetValues
(ServerGlobalVariables.OwinChallengeFlag);
context.Response.StatusCode =
Convert.ToInt16(headerValues.FirstOrDefault());
context.Response.Headers.Remove(
ServerGlobalVariables.OwinChallengeFlag);
}
}
}
最後に、Jeffが指摘したように、このカスタムOwinMiddlewareをStartup.Configuration
またはStartup.ConfigureAuth
メソッドに登録する必要があります。
app.Use<CustomAuthenticationMiddleware>();
上記のソリューションを使用して、次に示すようなステータスコードとカスタムエラーメッセージを設定できます。
3)ProtocolExceptionからエラーメッセージを抽出する
クライアントアプリケーションでは、ProtocolExceptionをキャッチして処理する必要があります。このような何かがあなたに答えを与えるでしょう:
//Need to create a class to deserialize the Json
//Create this somewhere in your application
public class OAuthErrorMsg
{
public string error { get; set; }
public string error_description { get; set; }
public string error_uri { get; set; }
}
//Need to make sure to include Newtonsoft.Json
using Newtonsoft.Json;
//Code for your object....
private void login()
{
try
{
var state = _webServerClient.ExchangeUserCredentialForToken(
this.emailTextBox.Text,
this.passwordBox.Password.Trim(),
scopes: new string[] { "PublicProfile" });
_accessToken = state.AccessToken;
_refreshToken = state.RefreshToken;
}
catch (ProtocolException ex)
{
var webException = ex.InnerException as WebException;
OAuthErrorMsg error =
JsonConvert.DeserializeObject<OAuthErrorMsg>(
ExtractResponseString(webException));
var errorMessage = error.error_description;
//Now it's up to you how you process the errorMessage
}
}
public static string ExtractResponseString(WebException webException)
{
if (webException == null || webException.Response == null)
return null;
var responseStream =
webException.Response.GetResponseStream() as MemoryStream;
if (responseStream == null)
return null;
var responseBytes = responseStream.ToArray();
var responseString = Encoding.UTF8.GetString(responseBytes);
return responseString;
}
私はこれをテストしましたが、それはVS2013 Pro 4.5で完全に動作します!!
(注意してください。必要な名前空間や追加のコードはすべて、アプリケーション(WPF、MVC、またはWinform)によって異なるため、ここには含めませんでした。また、エラー処理については説明しなかったので、ソリューション全体に適切なエラー処理を実装します。)
何時間もWebを検索してBLOBを読み、owinのドキュメントを読んだ後、失敗したログイン試行に対して401を返す方法を見つけました。
以下のヘッダーを追加するのは少しハックのようですが、IOwinContext.Response.Bodyストリームを読み取ってエラーメッセージを探す方法が見つかりませんでした。
まず、_OAuthAuthorizationServerProvider.GrantResourceOwnerCredentials
_でSetError()
を使用し、Headers
を応答に追加しました
_context.SetError("Autorization Error", "The username or password is incorrect!");
context.Response.Headers.Add("AuthorizationResponse", new[] { "Failed" });
_
これで、失敗した認証リクエストの400エラーと、他の何かが原因で発生した400エラーを区別する方法があります。
次のステップは、OwinMiddleware
を継承するクラスを作成することです。このクラスは発信応答をチェックし、_StatusCode == 400
_および上記のヘッダーが存在する場合、StatucCodeを401に変更します。
_public class InvalidAuthenticationMiddleware : OwinMiddleware
{
public InvalidAuthenticationMiddleware(OwinMiddleware next)
: base(next)
{
}
public override async Task Invoke(IOwinContext context)
{
await Next.Invoke(context);
if (context.Response.StatusCode == 400 && context.Response.Headers.ContainsKey("AuthorizationResponse"))
{
context.Response.Headers.Remove("AuthorizationResponse");
context.Response.StatusCode = 401;
}
}
}
_
最後に、_Startup.Configuration
_メソッドで、作成したクラスを登録します。メソッドで他のことをする前に登録しました。
_app.Use<InvalidAuthenticationMiddleware>();
_
ジェフの解決策は私にはうまくいきませんが、OnSendingHeaders
を使用するとうまくいきます:
public class InvalidAuthenticationMiddleware : OwinMiddleware
{
public InvalidAuthenticationMiddleware(OwinMiddleware next) : base(next) { }
public override async Task Invoke(IOwinContext context)
{
context.Response.OnSendingHeaders(state =>
{
var response = (OwinResponse)state;
if (!response.Headers.ContainsKey("AuthorizationResponse") && response.StatusCode != 400) return;
response.Headers.Remove("AuthorizationResponse");
response.StatusCode = 401;
}, context.Response);
await Next.Invoke(context);
}
}