Visual Studioで新しいWebAPIソリューションを作成し、コードをいじって何が起こっているのかを理解しようとしています。
承認コントローラーと実際のすべての機能を実装する別のコントローラーですべて稼働しているテストAPIがあります。
/ Tokenリクエストを除いて、コントローラー(API)はすべてJSONを受信し、JSONで応答することで機能します。これは次のようにする必要があります。
Content-Type: application/x-www-form-urlencoded
そうしないと、エラーが返されます。
このエンドポイントを作成するコードのセクションは次のようになります。
OAuthOptions = new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/Token"),
Provider = new ApplicationOAuthProvider(PublicClientId),
AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
// In production mode set AllowInsecureHttp = false
AllowInsecureHttp = false
};
このように呼び出すと、ベアラートークンを使用して200の成功応答が返されます。
$("#token_button").click(function ()
{
var username = $("#token_email").val();
var password = $("#token_password").val();
postData("Token", "grant_type=password&username=" + username + "&password=" + password, "application/x-www-form-urlencoded", function (data)
{
user = data;
$("#feedback_display").html(user.access_token);
}, function ()
{
user = null;
});
});
このように呼び出すと、400応答になります。
$("#token_button").click(function ()
{
var username = $("#token_email").val();
var password = $("#token_password").val();
var data = {
"grant_type": "password",
"username": username,
"password": password
}
postData("Token", JSON.stringify(data), "application/json", function (data)
{
user = data;
$("#feedback_display").html(user.access_token);
}, function ()
{
user = null;
});
});
応答本文は次のとおりです。
{"error":"unsupported_grant_type"}
ここでの唯一の違いは、リクエストの送信に使用されるエンコーディングです。すべての例を見ると、フォームエンコーディングを使用してこのトークンを要求しています。
/ api/Account/ExternalLoginの下のコードにブレークポイントを設定しても、ヒットすることはありません。
これがフォームエンコーディングのみを受け入れる理由はありますか?そうでない場合は、JSONを受け入れるようにコントローラーを変更するにはどうすればよいですか?
あるいは、私は愚かなことをしたことがありますか?
application/x-www-form-urlencoded
をContent-Type
として使用する理由は単純です。 OAuth2仕様(RFC 6749) は、トークン要求にこのコンテンツタイプを必要とします。
その他のコンテンツタイプは、OAuth2準拠のクライアントの互換性を損ないます。この標準的な動作を変更しないことをお勧めします。
注
これに注意してください:
postData("Token", data, "application/json", function (data)
{
//...
}
jSONを送信していないという理由だけで機能します! application/json
をContent-Type
ヘッダーとして追加した場合でも、リクエストの本文はフォームのキーと値のペアとしてシリアル化されます(AJAX呼び出しでのjQueryのデフォルトオブジェクトのシリアル化)。
Microsoft.Owin.Security.OAuth
からのOAuthAuthorizationServerMiddleware
(より正確には内部で使用される OAuthAuthorizationServerHandler
)のデフォルトの実装は、Content-Type
ヘッダーを無視し、とにかくフォームとして本文をリクエストしてください。
データを直接JSON.stringify(data)
渡す必要はありません。
顧客の技術スペシャリストが、/ tokenエンドポイントで「application/x-www-form-urlencoded」形式と「application/json」形式の両方を本文で使用できるように要求しました。そのため、仕様に反しているにもかかわらず、実装する必要がありました。
パスが「/ api/token」で、content-typeが「application/json」の場合、JSON本体をUrlエンコードされた本体に変換するOwinミドルウェアを作成します。 Startup.csに登録することを忘れないでください。
public sealed class JsonBodyToUrlEncodedBodyMiddleware : OwinMiddleware
{
public JsonBodyToUrlEncodedBodyMiddleware(OwinMiddleware next)
: base(next)
{
}
public override async Task Invoke(IOwinContext context)
{
if (string.Equals(context.Request.ContentType, "application/json")
&& string.Equals(context.Request.Method, "POST", StringComparison.InvariantCultureIgnoreCase)
&& context.Request.Path == new PathString("/avi/token/"))
{
try
{
await ReplaceJsonBodyWithUrlEncodedBody(context);
await Next.Invoke(context);
}
catch (Exception)
{
context.Response.StatusCode = (int) HttpStatusCode.BadRequest;
context.Response.Write("Invalid JSON format.");
}
}
else
{
await Next.Invoke(context);
}
}
private async Task ReplaceJsonBodyWithUrlEncodedBody(IOwinContext context)
{
var requestParams = await GetFormCollectionFromJsonBody(context);
var urlEncodedParams = string.Join("&", requestParams.Select(kvp => $"{kvp.Key}={kvp.Value}"));
var decryptedContent = new StringContent(urlEncodedParams, Encoding.UTF8, "application/x-www-form-urlencoded");
var requestStream = await decryptedContent.ReadAsStreamAsync();
context.Request.Body = requestStream;
}
private static async Task<Dictionary<string, string>> GetFormCollectionFromJsonBody(IOwinContext context)
{
context.Request.Body.Position = 0;
var jsonString = await new StreamReader(context.Request.Body).ReadToEndAsync();
var requestParams = JsonConvert.DeserializeObject<Dictionary<string, string>>(jsonString);
return requestParams;
}
}
OAuth2では、トークンリクエストにapplication/x-www-form-urlencoded
コンテンツタイプが必要です。
それでも、私はこの回避策について考えました:
// GET api/Account/GetToken
[HttpPost]
[AllowAnonymous]
[Route("GetToken")]
public async Task<IHttpActionResult> GetToken(TokenRequest request)
{
var client = new HttpClient()
{
BaseAddress = new Uri(Request.RequestUri.GetLeftPart(UriPartial.Authority))
};
var content = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("grant_type", "password"),
new KeyValuePair<string, string>("username", request.Username),
new KeyValuePair<string, string>("password", request.Password)
});
var result = await client.PostAsync("/token", content);
string resultContent = await result.Content.ReadAsStringAsync();
resultContent = resultContent.Replace(".issued", "issued").Replace(".expires", "expires");
TokenResponse tokenResponse = JsonConvert.DeserializeObject<TokenResponse>(resultContent);
return Ok(tokenResponse);
}
モデル:
public class TokenRequest
{
public string Username { get; set; }
public string Password { get; set; }
}
public class TokenResponse
{
public string access_token { get; set; }
public string token_type { get; set; }
public int expires_in { get; set; }
public string userName { get; set; }
public DateTime issued { get; set; }
public DateTime expires { get; set; }
public string error { get; set; }
public string error_description { get; set; }
}
改善することはできますが、うまく機能します。