I'm lost trying to understand how Dart shelf executes the middleware and handlers. From all the documentation I have read (and briefing it up) if you write a Middleware that returns null, then the execution goes down the pipeline.
Otherwise if the middleware returns a Response, then the execution down the pipeline is stopped, and the Response is returned to the caller.
I have a server with a simple pipeline like this:
var handler = const shelf.Pipeline()
.addMiddleware(shelf.logRequests())
//.addMiddleware(options)
.addMiddleware(auth)
.addHandler(Router.handle);
The auth middleware checks 3 cases: Register, Login and Verify.
Register -> Creates new user and returns Response.ok(token), or if nor possible Response.InternalServerError
Login -> Refreshes the token and returns Response.ok(token), or if not correct Response(401)
Verify -> Returns null when ok(should continue down the pipeline), or Response(403, forbidden)
The problem is, that I cannot stop the execution of the middlewares. If I make a successful login, still the program goes down the pipeline and calls the Router. Which of course doesn't have the path for register and returns 404 as it is expected to do.
According to shelf documentation, it is supposed to stop when a middleware returns a response. What the hell am I doing wrong?
This is the code of the auth Middleware for reference:
abstract class AuthProvider {
static JsonDecoder _decoder = const JsonDecoder();
static FutureOr<Response> handle(Request request) async {
print('Entering auth middleware');
if(request.url.toString() == 'login'){
print('into login from auth');
AuthProvider.auth(request);
}
else if(request.url.toString() == 'register'){
print('Into register from auth');
RegisterController.handle(request);
}
else {
print('Into verify from auth');
AuthProvider.verify(request);
}
}
static FutureOr<Response> auth(Request request) async {
print('Entering auth');
String sql;
var query = ExecQuery();
try {
dynamic data = jsonDecode(await request.readAsString()) as Map<String, dynamic>;
final user = data['email'].toString();
final hash = Hash.create(data['password'].toString());
sql =
'''SELECT COUNT(*) FROM public.user WHERE (email = '${user}' AND password = '${hash}')''';
await query.countSql(sql);
if (query.result.status && query.result.opResult[0][0] == 1) {
JwtClaim claim = JwtClaim(
subject: user,
issuer: 'Me',
audience: ['users'],
);
final token = issueJwtHS256(claim, config.secret);
sql = '''UPDATE public.user SET token = '${token}'
WHERE (email = '${user}' AND password = '${hash}')''';
await query.rawQuery(sql);
return Response.ok(token);
}
else{throw Exception();}
} catch (e) {
return Response(401, body: 'Incorrect username/password');
}
}
static FutureOr<Response> verify(Request request) async {
print('Entering verify');
try {
final token = request.headers['Authorization'].replaceAll('Bearer ', '');
print('Received token: ${token}');
final claim = verifyJwtHS256Signature(token, config.secret);
print('got the claim');
claim.validate(issuer: 'ACME Widgets Corp',
audience: 'homacenter');
print ('returning null in middleware');
return null;
} catch(e) {
print(e.toString());
return Response.forbidden('Authorization rejected');
}
}
}
I reply myself... after losing days in this, a return was missing, that made the pipeline keep going. Issue closed.
abstract class AuthProvider {
static JsonDecoder _decoder = const JsonDecoder();
static FutureOr<Response> handle(Request request) async {
if(request.url.toString() == 'login'){
return AuthProvider.auth(request);
}
else if(request.url.toString() == 'register'){
return RegisterController.handle(request);
}
else {
return AuthProvider.verify(request);
}
}
Related
According to this documentation, https://developers.google.com/identity/oauth2/web/guides/use-token-model , which says:
Token expiration
By design, access tokens have a short lifetime. If the access token expires prior to the end of the user's session, obtain a new token by calling requestAccessToken() from a user-driven event such as a button press.
This steers us away from using a refresh token. That is Question 0: when should a refresh token be used?
My app is a web app and users stay signed in for a long time -> longer than access token expiry time, which seems to be one hour. To make it a little easier to continually fetch fresh access tokens as they are required, I wrote this code below.
It is intended to enable me to make statements like:
const provider = new GoogleAccessTokenProvider(clientId)
const scopes = ["https://www.googleapis.com/auth/drive.metadata.readonly"]
provider.getTokenWithScopes(scopes)
.then(token => {
console.log(token)
}).catch(e => {
console.log(e)
})
It seems like I have to do a lot of uninteresting stuff in this code, so I have many questions about it, listed below:
Question 1 - It stores the token response from google.accounts.oauth2 in local storage. Is this the kind of thing Google intended us to do - store the access token locally?
Question 2 - It manages token expiry (with a ten second buffer). Again, is this something Google intend us to concern ourselves with? Is there a lib that does this automatically?
Question 3 - If calls requestAccessToken() with no prompt if the access token has the required scopes, but it is expired. This flashes up the Google consent dialog for a second, then it disappears. This seems to me to be where a refresh token would be handy.
Anyway, it works, but this flash up of the consent dialog is not great UX - is this what Google intended us to do?
Question 4 - It calls initTokenClient for every requestAccessToken(), because scopes are bound to this call. Seems odd to me - is this what Google intended us to do?
Anyway, here is the code:
class LocalStorage {
getItem(key: string): string | null {
return window.localStorage.getItem(key)
}
setItem(key: string, value: string): void {
return window.localStorage.setItem(key, value)
}
}
class TokenResponseStorage {
constructor(private readonly localStorage = new LocalStorage()) {
}
getItem(key: string): WrappedGoogleAccessTokenResponse | null {
const value = this.localStorage.getItem(key)
if (value === null) {
return null
}
return JSON.parse(value)
}
setItem(key: string, value: WrappedGoogleAccessTokenResponse): void {
this.localStorage.setItem(key, JSON.stringify(value))
}
}
interface WrappedGoogleAccessTokenResponse {
_type: "wrapped.google.access.token.response"
expiresAtMillis: number
tokenResponse: google.accounts.oauth2.TokenResponse
}
export class GoogleAccessTokenProvider {
private readonly expiryMarginMillis = 1000 * 10 // ten seconds before expiry
constructor(private readonly clientId: string,
private readonly storage = new TokenResponseStorage(),
private readonly storageKey = 'docsndata.google.access.token') {
}
async getTokenWithScopes(scopes: string[]): Promise<string> {
return new Promise(resolve => {
const wrappedToken = this.storage.getItem(this.storageKey)
if (wrappedToken && scopes.every(s => wrappedToken.tokenResponse.scopes.includes(s))) {
if (this._hasSufficientExpiry(wrappedToken)) {
resolve(wrappedToken.tokenResponse.access_token)
} else {
this._promptForToken(scopes, 'none').then(resolve)
}
} else {
this._promptForToken(scopes, 'consent').then(resolve)
}
})
}
private _hasSufficientExpiry(token: WrappedGoogleAccessTokenResponse): boolean {
return token.expiresAtMillis - this.expiryMarginMillis > Date.now()
}
private async _promptForToken(scopes: string[], prompt: "none" | "consent"): Promise<string> {
const that = this
return new Promise(resolve => {
const tokenClient = google.accounts.oauth2.initTokenClient({
client_id: this.clientId,
scope: scopes.join(' '),
callback: function (tokenResponse) {
that._storeTokenResponse(tokenResponse)
resolve(tokenResponse.access_token)
}
})
tokenClient.requestAccessToken({prompt})
})
}
private _storeTokenResponse(tokenResponse: google.accounts.oauth2.TokenResponse) {
const wrappedTokenResponse: WrappedGoogleAccessTokenResponse = {
_type: "wrapped.google.access.token.response",
expiresAtMillis: Date.now() + parseInt(tokenResponse.expires_in) * 1000,
tokenResponse
}
this.storage.setItem(this.storageKey, wrappedTokenResponse)
}
}
I am using dart:io to create the server. I send the request from the Postman with form-data. I need to use form-data because my old API from another language uses it and the app uses it too.
At the moment. I am trying to get the data and files with this code:
Future main(List<String> arguments) async {
HttpServer server = await HttpServer.bind('localhost', 8085);
server.listen((HttpRequest request) async {
String jsonString = await request.cast<List<int>>().transform(utf8.decoder).join();
print("jsonString:\n$jsonString");
await request.response.close();
});
}
When I send the data and a file from the Postman with this below.
I will get the error below.
Unhandled exception:
FormatException: Unexpected extension byte (at offset 435)
If I don't send the file as image 1, I got this.
jsonString:
----------------------------166099235909119466948633
Content-Disposition: form-data; name="key 1"
Content-Type: application/json
value 1
----------------------------166099235909119466948633
Content-Disposition: form-data; name="key 2"
value 2
----------------------------166099235909119466948633--
I can't convert the above results to variables.
I don't know how to do that. Has anyone an example for doing this or suggest any package to me? This is my first time creating a dart server.
I follow this.
You can get the data and files from the request by using shelf_multipart (Other packages may be used in conjunction with this one and find more methods on GitHub).
If you want to see results quickly that it can be done. Follow this below.
I am using 3 packages including the shelf, shelf_router, and shelf_multipart packages.
You need to add these packages to your pubspec.yaml.
(You can copy and paste these into your pubspec.yaml.)
dependencies:
shelf: ^1.4.0
shelf_router: ^1.1.3
shelf_multipart: ^1.0.0
Then copy my code and past it to your main.dart:
import 'dart:convert';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as shelf_io;
import 'package:shelf_router/shelf_router.dart';
import 'package:shelf_multipart/form_data.dart';
import 'package:shelf_multipart/multipart.dart';
Future main(List<String> arguments) async {
final service = Service();
final server = await shelf_io.serve(service.handler, 'localhost', 8085);
print('Server running on localhost:${server.port}');
}
class Service {
Handler get handler {
final router = Router();
router.post("/example", (Request request) async {
if (request.isMultipart && request.isMultipartForm) {
Map<String, dynamic>? data = await RequestConverter.formData(request);
return data != null
? Response.ok("form-data: true, receive-data: true, data: $data")
: Response.ok("form-data: true, receive-data: false");
}
return Response.ok("form-data: false");
});
router.all('/<ignored|.*>', (Request request) {
return Response.notFound('Page not found');
});
return router;
}
}
class RequestConverter {
static Future<Map<String, dynamic>?> formData(Request request) async {
try {
Map<String, dynamic> data = {};
Map<String, dynamic> files = {};
final List<FormData> formDataList = await request.multipartFormData.toList();
for (FormData formData in formDataList) {
if (formData.filename == null) {
String dataString = await formData.part.readString();
data[formData.name] = Json.tryDecode(dataString) ?? dataString; //Postman doesn't send data as json
} else if (formData.filename is String) {
files[formData.name] = await formData.part.readBytes();
}
}
return {"data": data, "files": files};
} catch (e) {
return null;
}
}
}
class Json {
static String? tryEncode(data) {
try {
return jsonEncode(data);
} catch (e) {
return null;
}
}
static dynamic tryDecode(data) {
try {
return jsonDecode(data);
} catch (e) {
return null;
}
}
}
After this, you can start your server in your terminal. For me I am using:
dart run .\bin\main.dart
Finally, open the Postman and paste http://localhost:8085/example to the URL field, select the POST method, and form-data. You can add the data into the KEY and VALUE fields. Then press send.
This is my example in the Postman:
This solution work with http.MultipartRequest() from the Flutter app.
I'm currently trying to access a Web API in Flutter that requires a JWT access token for authorization. The access token expires after a certain amount of time.
A new access token can be requested with a separate refresh token. Right now this access token refresh is performed as soon as a request returns a 401 response. After that, the failed request should be retried with the new access token.
I'm having trouble with this last step. It seems like a http.BaseRequest can only be sent once. How would I retry the http request with the new token?
As suggested in the dart http readme, I created a subclass of http.BaseClient to add the authorization behavior. Here is a simplified version:
import 'dart:async';
import 'package:http/http.dart' as http;
class AuthorizedClient extends http.BaseClient {
AuthorizedClient(this._authService) : _inner = http.Client();
final http.Client _inner;
final AuthService _authService;
Future<http.StreamedResponse> send(http.BaseRequest request) async {
final token = await _authService.getAccessToken();
request.headers['Authorization'] = 'Bearer $token';
final response = await _inner.send(request);
if (response.statusCode == 401) {
final newToken = await _authService.refreshAccessToken();
request.headers['Authorization'] = 'Bearer $newToken';
// throws error: Bad state: Can't finalize a finalized Request
final retryResponse = await _inner.send(request);
return retryResponse;
}
return response;
}
}
abstract class AuthService {
Future<String> getAccessToken();
Future<String> refreshAccessToken();
}
Here is what I came up with so far, based on Richard Heap's answer: To resend a request, we have to copy it.
So far I was not able to come up for a solution for stream requests!
http.BaseRequest _copyRequest(http.BaseRequest request) {
http.BaseRequest requestCopy;
if(request is http.Request) {
requestCopy = http.Request(request.method, request.url)
..encoding = request.encoding
..bodyBytes = request.bodyBytes;
}
else if(request is http.MultipartRequest) {
requestCopy = http.MultipartRequest(request.method, request.url)
..fields.addAll(request.fields)
..files.addAll(request.files);
}
else if(request is http.StreamedRequest) {
throw Exception('copying streamed requests is not supported');
}
else {
throw Exception('request type is unknown, cannot copy');
}
requestCopy
..persistentConnection = request.persistentConnection
..followRedirects = request.followRedirects
..maxRedirects = request.maxRedirects
..headers.addAll(request.headers);
return requestCopy;
}
You can't send the same BaseRequest twice. Make a new BaseRequest from the first one, and send that copy.
Here's some code (from io_client) to 'clone' a BaseRequest.
var copyRequest = await _inner.openUrl(request.method, request.url);
copyRequest
..followRedirects = request.followRedirects
..maxRedirects = request.maxRedirects
..contentLength = request.contentLength == null
? -1
: request.contentLength
..persistentConnection = request.persistentConnection;
request.headers.forEach((name, value) {
copyRequest.headers.set(name, value);
});
I am trying to implement twitter sign in/up. In a asp.net web app, but i am getting 403 http status on the final callback.
I have my callback urls configured in the twitter app portal (I think they are correct)
I give a little bit of context of what i am trying to do
Redict the user to the twitter sining
Then the first callback executes (no issue here) and i call the twitter api to get the user details.
After getting the user details i return a challenge result so i can get the user identity and i specify a second callback for that
The second callback does not execute.
Does somebody can point out to me what am I doing wrong? Or how can i debug the issue?
I am aware that twitter checks that the callback url needs to be set in the app developer portal I got that from this question question
Here's my code and config
app.UseTwitterAuthentication(new TwitterAuthenticationOptions()
{
ConsumerKey = "key",
ConsumerSecret = "qCLLsuS79YDkmr2DGiyjruV76mWZ4hVZ4EiLU1RpZkxOfDqwmh",
Provider = new Microsoft.Owin.Security.Twitter.TwitterAuthenticationProvider
{
OnAuthenticated = (context) =>
{
context.Identity.AddClaim(new System.Security.Claims.Claim("urn:twitter:access_token", context.AccessToken));
context.Identity.AddClaim(new System.Security.Claims.Claim("urn:twitter:access_secret", context.AccessTokenSecret));
return Task.FromResult(0);
}
},
BackchannelCertificateValidator = new Microsoft.Owin.Security.CertificateSubjectKeyIdentifierValidator(new[]
{
"A5EF0B11CEC04103A34A659048B21CE0572D7D47", // VeriSign Class 3 Secure Server CA - G2
"0D445C165344C1827E1D20AB25F40163D8BE79A5", // VeriSign Class 3 Secure Server CA - G3
"7FD365A7C2DDECBBF03009F34339FA02AF333133", // VeriSign Class 3 Public Primary Certification Authority - G5
"39A55D933676616E73A761DFA16A7E59CDE66FAD", // Symantec Class 3 Secure Server CA - G4
"add53f6680fe66e383cbac3e60922e3b4c412bed", // Symantec Class 3 EV SSL CA - G3
"4eb6d578499b1ccf5f581ead56be3d9b6744a5e5", // VeriSign Class 3 Primary CA - G5
"5168FF90AF0207753CCCD9656462A212B859723B", // DigiCert SHA2 High Assurance Server CA
"B13EC36903F8BF4701D498261A0802EF63642BC3" // DigiCert High Assurance EV Root CA
}),
});
Calling twitter sign in (I specify the first callback url and this one works )
[AllowAnonymous]
public ActionResult TwitterRegistration()
{
string UrlPath = HttpContext.Request.Url.Authority;
// pass in the consumerkey, consumersecret, and return url to get back the token
NameValueCollection dict = new TwitterClient().GenerateTokenUrl(ConsumerKey, ConsumerSecret, "https://" + UrlPath + "/Account/TwitterRegistrationCallback");
// set a session var so we can use it when twitter calls us back
Session["dict"] = dict;
// call "authenticate" not "authorize" as the twitter docs say so the user doesn't have to reauthorize the app everytime
return Redirect("https://api.twitter.com/oauth/authenticate?oauth_token=" + dict["oauth_token"]);
}
After the callback I call the twitter api to get the user data that works too
[AllowAnonymous]
public ActionResult TwitterRegistrationCallback(string oauth_token, string oauth_verifier)
{
TwitterClient twitterClient = new TwitterClient();
NameValueCollection dict = (NameValueCollection)Session["dict"];
NameValueCollection UserDictionary = HttpUtility.ParseQueryString(twitterClient.GetAccessToken(ConsumerKey, ConsumerSecret, oauth_token, oauth_verifier, dict));
TwitterUserModel twitterUser = JsonConvert.DeserializeObject<TwitterUserModel>(twitterClient.GetTwitterUser(ConsumerKey, ConsumerSecret, UserDictionary));
Session["twitterUser"] = twitterUser;
// Returning challenge not working just redirecting to the action inn case of twitter as we are already authenitcated
return new ChallengeResult("Twitter", Url.Action("ExternalRegistrationCallback", "Account", null));
}
But when I return the Challange result which ends up calling
context.HttpContext.GetOwinContext().Authentication.Challenge(properties, LoginProvider);
it gives me the exception below (which is the same in the original question)
Here is the callback that is not being called
// GET: /Account/ExternalRegistrationCallback
[AllowAnonymous]
public async Task<ActionResult> ExternalRegistrationCallback()
{
//TODO: Check
if (User.Identity.IsAuthenticated)
{
return RedirectToAction("Index", "Manage");
}
var loginInfo = await _authenticationManager.GetExternalLoginInfoAsync();
if (Session["twitterUser"] != null)
{
//Workarround for twitter registration callback not using the challenge
loginInfo = new ExternalLoginInfo();
TwitterUserModel twitterUser = (TwitterUserModel)Session["twitterUser"];
loginInfo.Email = twitterUser.email;
}
if (loginInfo == null)
{
return RedirectToAction("Login");
}
// Get the information about the user from the external login provider
var info = await _authenticationManager.GetExternalLoginInfoAsync();
if (info == null)
{
return View("ExternalLoginFailure");
}
// Sign in the user with this external login provider if the user already has a login
var result = await _signInManager.ExternalSignInAsync(loginInfo, isPersistent: false);
switch (result)
{
case SignInStatus.Success:
//User is already registered We show error and tell the user to go back to login page?
return RedirectToLocal((string)Session["ReturnUrl"]);
case SignInStatus.LockedOut:
return View("Lockout");
case SignInStatus.RequiresVerification:
//
return RedirectToAction("SendCode", new { ReturnUrl = (string)Session["ReturnUrl"], RememberMe = false });
case SignInStatus.Failure:
default:
// User is authenticated through the previous challange, So here needs to be saved
RegistrationBasicViewModel model = (RegistrationBasicViewModel)Session["RegistrationModel"];
//Check the user is in our db?
ApplicationUser user = _userManager.FindByEmail(loginInfo.Email);
IdentityResult identityResult;
if (user == null)
{
user = new ApplicationUser
{
UserName = loginInfo.Email,
Email = loginInfo.Email,
FirstName = model.FirstName,
LastName = model.LastName,
Nickname = model.Nickname
};
identityResult = await _userManager.CreateAsync(user);
}
else
{
//TODO : Here we might want to tell the user it already exists
identityResult = IdentityResult.Success;
//IdentityResult.Failed(new string[] { "User already registered" });
}
if (identityResult.Succeeded)
{
identityResult = await _userManager.AddLoginAsync(user.Id, info.Login);
if (identityResult.Succeeded)
{
//Adding the branch after te user is sucessfully added
await _signInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false);
_userBranchService.AddUserBranch(user.Id, model.BranchId);
//Redirect to home page
return RedirectToLocal((string)Session["ReturnUrl"]);
}
}
setPartnerBranchViewBag(model.PartnerId, (string) Session["partner"]);
AddErrors(identityResult);
return View("Register", model );
}
}
Twitter config
[HttpRequestException: Response status code does not indicate success: 403 (Forbidden).]
System.Net.Http.HttpResponseMessage.EnsureSuccessStatusCode() +223
Microsoft.Owin.Security.Twitter.<ObtainRequestTokenAsync>d__23.MoveNext(
Apparently Owin uses a default url (not the url set on the Challange)
The default url is /signin-twitter So in my case i had to configure https://localhost:44378/signin-twitter as one of the callback urls in the twitter app portal
Even after adding the /signin-twitter to my callback url's, I receive the "Response status code does not indicate success: 403 (Forbidden)." error.
[HttpRequestException: Response status code does not indicate success: 403 (Forbidden).]
System.Net.Http.HttpResponseMessage.EnsureSuccessStatusCode() +121662
Microsoft.Owin.Security.Twitter.<ObtainRequestTokenAsync>d__23.MoveNext() +2389
System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() +31
System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) +60
Microsoft.Owin.Security.Twitter.<ApplyResponseChallengeAsync>d__12.MoveNext() +1091
This exception is thrown, even when using the default, out of the box Asp.NET MVC template.
I am trying to set the redirect_uri for the facebook login with Asp.Net Identity. However, the GetExternalLogin REST method in the AccountController is only triggered if the redirect_uri is '/'. If I add anything else it does not trigger GetExternalLogin, the browser only shows error: invalid_request.
However the url contains the redirected parameter as it should e.g. if I add the redirect_uri as http://localhost:25432/testing
the response URL looks like this:
http://localhost:25432/api/Account/ExternalLogin?provider=Facebook&response_type=token&client_id=self&redirect_uri=http%3A%2F%2Flocalhost%3A25432%2Ftesting&state=0NctHHGq_aiazEurHYbvJT8hDgl0GJ_GGSdFfq2z5SA1
and the browser window shows: error: invalid_request
Any idea why this works only when redirecting to '/' but not to any other url´s?
For anyone else that might run into this issue: the problem is when you take (copy) the ApplicationOAuthProvider.cs from the Visual Studio SPA template and it is there where this code is:
public override Task ValidateClientRedirectUri(OAuthValidateClientRedirectUriContext context)
{
if (context.ClientId == _publicClientId)
{
var expectedRootUri = new Uri(context.Request.Uri, "/");
if (expectedRootUri.AbsoluteUri == context.RedirectUri)
{
context.Validated();
}
}
return Task.FromResult<object>(null);
}
This will obviously block any redirect_uri that doesn't look like http://localhost/ or http://example.com/ so for instance http://example.com/home won't work.
Now this below is the source for InvokeAuthorizeEndpointAsync in Katana which does all the work and you can see it calls into any custom OAuthProvider that might be registered for this MVC/Web API application (this registration typically happens in Startup.Auth.cs):
private async Task<bool> InvokeAuthorizeEndpointAsync()
{
var authorizeRequest = new AuthorizeEndpointRequest(Request.Query);
var clientContext = new OAuthValidateClientRedirectUriContext(
Context,
Options,
authorizeRequest.ClientId,
authorizeRequest.RedirectUri);
if (!String.IsNullOrEmpty(authorizeRequest.RedirectUri))
{
bool acceptableUri = true;
Uri validatingUri;
if (!Uri.TryCreate(authorizeRequest.RedirectUri, UriKind.Absolute, out validatingUri))
{
// The redirection endpoint URI MUST be an absolute URI
// http://tools.ietf.org/html/rfc6749#section-3.1.2
acceptableUri = false;
}
else if (!String.IsNullOrEmpty(validatingUri.Fragment))
{
// The endpoint URI MUST NOT include a fragment component.
// http://tools.ietf.org/html/rfc6749#section-3.1.2
acceptableUri = false;
}
else if (!Options.AllowInsecureHttp &&
String.Equals(validatingUri.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase))
{
// The redirection endpoint SHOULD require the use of TLS
// http://tools.ietf.org/html/rfc6749#section-3.1.2.1
acceptableUri = false;
}
if (!acceptableUri)
{
clientContext.SetError(Constants.Errors.InvalidRequest);
return await SendErrorRedirectAsync(clientContext, clientContext);
}
}
await Options.Provider.ValidateClientRedirectUri(clientContext);
if (!clientContext.IsValidated)
{
_logger.WriteVerbose("Unable to validate client information");
return await SendErrorRedirectAsync(clientContext, clientContext);
}
var validatingContext = new OAuthValidateAuthorizeRequestContext(
Context,
Options,
authorizeRequest,
clientContext);
if (string.IsNullOrEmpty(authorizeRequest.ResponseType))
{
_logger.WriteVerbose("Authorize endpoint request missing required response_type parameter");
validatingContext.SetError(Constants.Errors.InvalidRequest);
}
else if (!authorizeRequest.IsAuthorizationCodeGrantType &&
!authorizeRequest.IsImplicitGrantType)
{
_logger.WriteVerbose("Authorize endpoint request contains unsupported response_type parameter");
validatingContext.SetError(Constants.Errors.UnsupportedResponseType);
}
else
{
await Options.Provider.ValidateAuthorizeRequest(validatingContext);
}
if (!validatingContext.IsValidated)
{
// an invalid request is not processed further
return await SendErrorRedirectAsync(clientContext, validatingContext);
}
_clientContext = clientContext;
_authorizeEndpointRequest = authorizeRequest;
var authorizeEndpointContext = new OAuthAuthorizeEndpointContext(Context, Options);
await Options.Provider.AuthorizeEndpoint(authorizeEndpointContext);
return authorizeEndpointContext.IsRequestCompleted;
}
This is key:
await Options.Provider.ValidateClientRedirectUri(clientContext);
So your solution is to change how the ValidateClientRedirectUri performs the validation - the default SPA implementation is, as you can see, very naive.
There's lots of ppl having issues with SPA mainly because it lacks any kind of useful information and I mean that both for ASP.NET Identity and OWIN stuff and with regards to what is going on within KnockoutJS implementation.
I wish Microsoft would provide more comprehensive docs for these templates because anyone who will try to do anything a bit more complex will run into issues.
I've spent hours on this, digging into OWIN (Katana) source code thinking it is the above implementation that blocks my redirect URIs but it was not, hopefully helps someone else too.
HTH
The problem is that GetExternalLogin registered as OAuthOptions.AuthorizeEndpointPath which used for app.UseOAuthBearerTokens(OAuthOptions). This configuration puts validation on arguments of endpoint.
if (!Uri.TryCreate(authorizeRequest.RedirectUri, UriKind.Absolute, out validatingUri))
{
// The redirection endpoint URI MUST be an absolute URI
}
else if (!String.IsNullOrEmpty(validatingUri.Fragment))
{
// The endpoint URI MUST NOT include a fragment component.
}
else if (!Options.AllowInsecureHttp &&
String.Equals(validatingUri.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase))
{
// The redirection endpoint SHOULD require the use of TLS
}
And you should pass "Authorize endpoint request missing required response_type parameter" and
"Authorize endpoint request contains unsupported response_type parameter"
Based on the other answers, I changed the Validation code in ApplicationOAuthProvider.cs to just ensure that the redirect uri is on the same domain like so:
public override Task ValidateClientRedirectUri(OAuthValidateClientRedirectUriContext context)
{
if (context.ClientId == _publicClientId)
{
Uri expectedRootUri = new Uri(context.Request.Uri, "/");
if (context.RedirectUri.StartsWith(expectedRootUri.AbsoluteUri))
{
context.Validated();
}
}
return Task.FromResult<object>(null);
}