0

I'm trying to make a sign-in (Angular2 - client and web API 2 - on a server).

I'm receiving a

OPTIONS http://localhost:48604/Token 400 (Bad Request)

followed by

Failed to load http://localhost:48604/Token: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:4200' is therefore not allowed access. The response had HTTP status code 400.

while other requests don't fail due to CORS issue because I've got CORS enabled globally in WebApiConfig.cs:

Web API:

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        // Web API configuration and services
        // Configure Web API to use only bearer token authentication.
        config.SuppressDefaultHostAuthentication();
        config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
        EnableCorsAttribute cors = new EnableCorsAttribute("http://localhost:4200", "*", "*");
        config.EnableCors(cors);
        // Web API routes
        config.MapHttpAttributeRoutes();

        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );


    }
}

My Startup.Auth.cs :

public partial class Startup
{
    public static OAuthAuthorizationServerOptions OAuthOptions { get; private set; }

    public static string PublicClientId { get; private set; }

    // For more information on configuring authentication, please visit https://go.microsoft.com/fwlink/?LinkId=301864
    public void ConfigureAuth(IAppBuilder app)
    {
        app.CreatePerOwinContext(ApplicationDbContext.Create);
        app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
        app.CreatePerOwinContext<ApplicationRoleManager>(ApplicationRoleManager.Create);

        app.UseCookieAuthentication(new CookieAuthenticationOptions());
        app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);

        // Configure the application for OAuth based flow
        PublicClientId = "self";
        OAuthOptions = new OAuthAuthorizationServerOptions
        {
            TokenEndpointPath = new PathString("/Token"),
            Provider = new ApplicationOAuthProvider(PublicClientId),
            AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
            AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
            AllowInsecureHttp = true
        };

        // Enable the application to use bearer tokens to authenticate users
        app.UseOAuthBearerTokens(OAuthOptions);

        // ... Code for third-part logins omitted for brevity ...
    }
}

My angular service:

export class AuthService {

rootUrl = 'http://localhost:48604';

httpOptions = {
    headers: new HttpHeaders({
        'Content-Type': 'application/x-www-form-urlencoded'
    })
};

constructor(private httpClient: HttpClient) {
}

logIn(loginData): Observable<any> {

    const data = 'grant_type=password&username=' + loginData.username + '&password=' + loginData.password;
    return this.httpClient.post(this.rootUrl + '/Token', data, this.httpOptions);
}
}

And as far as I understand after /Token the request should be redirected to api/Account/ExternalLogin but never gets to this controller method.

Then I found an post where they say you need to override MatchEndpoint method in ApplicationOAuthProvider class and I did it:

        public override Task MatchEndpoint(OAuthMatchEndpointContext context)
    {
        if (context.IsTokenEndpoint && context.Request.Method == "OPTIONS")
        {
            context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { "http://localhost:4200" });
            context.OwinContext.Response.Headers.Add("Access-Control-Allow-Headers", new[] { "authorization"});
            context.RequestCompleted();

            return Task.FromResult(0);
        }

        return base.MatchEndpoint(context);
    }

in my sign-in component:

    this.authService.logIn(loginData).subscribe(
  (data) => {
    console.log(data);
    //sessionStorage.setItem('tokenKey', token);
    //console.log('sessionStorage.getItem: ' + sessionStorage.getItem('tokenKey'));
  },

);

now I the response to POST request is 200 OK but still console says:

Failed to load http://localhost:48604/Token: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:4200' is therefore not allowed access.

then I add another if for a POST method where I also add needed headers:

    public override Task MatchEndpoint(OAuthMatchEndpointContext context)
    {
        if (context.IsTokenEndpoint && context.Request.Method == "OPTIONS")
        {
            context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin",
                new[] {"http://localhost:4200"});
            context.OwinContext.Response.Headers.Add("Access-Control-Allow-Headers", new[] {"authorization"});
            context.RequestCompleted();
            return Task.FromResult(0);
        }

        if (context.IsTokenEndpoint && context.Request.Method == "POST")
        {
            context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { "http://localhost:4200" });
            context.OwinContext.Response.Headers.Add("Access-Control-Allow-Headers", new[] { "authorization", "Content-Type" });
            context.RequestCompleted();
            return Task.FromResult(0);
        }
        return base.MatchEndpoint(context);
    }

Now the POST method will add headers to response but I get null instead of token.

Dmitriy Klyushin
  • 449
  • 1
  • 4
  • 22

2 Answers2

2

Ok. Problem solved. As explained in this video (31:00 minute)

So ASP.NET Identity uses OWIN and it needs to enable CORS in ConfigureAuth method of the Startup.cs file

public partial class Startup
{
    public static OAuthAuthorizationServerOptions OAuthOptions { get; private set; }

    public static string PublicClientId { get; private set; }

    // For more information on configuring authentication, please visit https://go.microsoft.com/fwlink/?LinkId=301864
    public void ConfigureAuth(IAppBuilder app)
    {

        app.CreatePerOwinContext(ApplicationDbContext.Create);
        app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
        app.CreatePerOwinContext<ApplicationRoleManager>(ApplicationRoleManager.Create);
        app.UseCors(CorsOptions.AllowAll);
        app.UseCookieAuthentication(new CookieAuthenticationOptions());
        app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);

        // Configure the application for OAuth based flow
        PublicClientId = "self";
        OAuthOptions = new OAuthAuthorizationServerOptions
        {
            TokenEndpointPath = new PathString("/Token"),
            Provider = new ApplicationOAuthProvider(PublicClientId),
            AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
            AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
            AllowInsecureHttp = true
        };

        // Enable the application to use bearer tokens to authenticate users
        app.UseOAuthBearerTokens(OAuthOptions);

        // ... Code for third-part logins omitted for brevity ...
    }
}

now to do this we need to install nuget package to the project:

Install-Package Microsoft.Owin.Cors

And I don't need to enable CORS in WebApiConfig file anymore:

EnableCorsAttribute cors = new EnableCorsAttribute("http://localhost:4200", "*", "*");
config.EnableCors(cors);

WebApiConfig file

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        // Web API configuration and services
        // Configure Web API to use only bearer token authentication.
        config.SuppressDefaultHostAuthentication();
        config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
       // EnableCorsAttribute cors = new EnableCorsAttribute("http://localhost:4200", "*", "*");
        // config.EnableCors(cors);
        // Web API routes
        config.MapHttpAttributeRoutes();

        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );


    }
}

I also don't need to override the MatchEndpoint method as I posted before.

Now when I make a sign-in request I receive the response with the token:

Request URL: http://localhost:48604/Token

and I see in response headers are added:

Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: http://localhost:4200

When I make a request to another API contoller say:

Request URL: http://localhost:48604/api/UpdateUsrRole

I see that first in the Request Method: OPTIONS these headers are added:

Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: content-type
Access-Control-Allow-Methods: PUT
Access-Control-Allow-Origin: http://localhost:4200

and then when the Request Method: PUT fires these headers are added:

Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: http://localhost:4200

So that's it.

Dmitriy Klyushin
  • 449
  • 1
  • 4
  • 22
0

You can add a proxy.config.json file to your angular application to allow for CORS, since Angular runs on port 4200 and your backend probably something else.

https://github.com/angular/angular-cli/blob/master/docs/documentation/stories/proxy.md

then serve via ng serve --proxy-config proxy.config.json

This will re-write any requests to the designated port etc. (Your backend)

If you're building your application all in 1 package, then you should not have a cors issue. ** Building angular then adding to static resources for backend **

Taranjit Kang
  • 2,510
  • 3
  • 20
  • 40
  • Yeah, 2 different ports then -- try with the proxy.config.json, and by 1 package I mean ng build then taking /dist into your backend and launching index.html via controller get route. – Taranjit Kang Jul 06 '18 at 14:55
  • 1) What do you mean "all in one package" ? It's like when .NetCore has a type of project template where angular is included? If you mean this - then no, I built my web API project separately and angular also separately with angular cli and they are running on different servers - web api - IIS (port 48604) and angular on the Angular Live Development Server (port 4200) 2) I think I got the idea with the proxy - but does it mean that I should use another server? The webpack-dev-server? 3) Will the code work when I deploy it to another server ... say amazon aws? Will those redirect set-ups work? – Dmitriy Klyushin Jul 06 '18 at 15:13
  • I also don't understand why other requests that I make succeed (GET/POST/PUT/DELETE) ? for ex. rootUrl = 'http://localhost:48604'; getAllUsers() { return this.httpClient.get(this.rootUrl + '/api/GetAllUsersRoles'); } will normally get to the server and will return a 200 ok with the data needed. What's the difference with the /Token target? Is this the common practice with Angular and web API to use proxy? I mean everyone tackle such problem and this is the way it should be done? – Dmitriy Klyushin Jul 06 '18 at 15:19
  • Proxy config is primarily for dev, when you deploy it won't work because the domain will change, currently both are ran on local host. So you will have to enable cors when you deploy. No other server is required, angular-cli already creates its own lite server. For token, it should just be a simple call and then store in local storage or sessionStorage as you have. – Taranjit Kang Jul 06 '18 at 15:25
  • thank you for help. found a solution without implementing proxy. posted below. – Dmitriy Klyushin Jul 08 '18 at 13:20