7

Ostatni tydzień spędziłem na tworzeniu interfejsu API dla istniejącej aplikacji MVC, a teraz próbuję zabezpieczyć interfejs API wraz z ponownym opracowaniem zabezpieczeń bocznych MVC w razie potrzeby .Żądanie POV MVC traci nagłówek autoryzacji - sposób użycia znacznika właściciela API po odzyskaniu

Obecnie aplikacja MVC jest skonfigurowana do używania pliku cookie aplikacji za pośrednictwem OWIN/OAuth/Identity. Próbowałem włączyć token na okaziciela, który jest skonfigurowany w interfejsie API sieci Web do generowania połączeń z ograniczonymi metodami API, ale dotychczas nie przyniósł sporego sukcesu - żądania GET działają dobrze, ale żądania POST tracą nagłówek autoryzacji po odebraniu przez API.

Utworzyłem klienta SDK, który jest używany przez aplikację MVC do wykonywania wywołań interfejsu API i próbowałem w sumie trzech metod ustawiania nagłówka autoryzacji dla dowolnego wywołania interfejsu API, z których wszystkie wydaje się działać dobrze dla żądań gET, ale całkowicie nie do jakichkolwiek żądań POST muszę zrobić ...

mogę ustawić nagłówek żądania w kontroler MVC:

HttpContext.Request.Headers. Dodaj ("Authorization", "Bearer" + response.AccessToken);

(gdzie response.AccessToken jest znak wcześniej pobierane z API)
mogę ustawić nagłówek żądania poprzez metodę rozszerzenia na Client SDK:

_apiclient.SetBearerAuthentication (token.AccessToken)

czy mogę ręcznie ustawić nagłówek żądania od Klienta SDK:

_apiClient.Authentication = new AuthenticationHeaderValue ("Nośnik, accessToken);

(gdzie accessToken jest tokenem pobranym wcześniej, przekazanym do wywoływanej metody Client).

Mam bardzo niewiele do omówienia z tego punktu, co jest przyczyną problemu. Jedyne, co udało mi się zebrać do tej pory, to to, że ASP.NET powoduje, że wszystkie żądania POST muszą najpierw wysłać żądanie z nagłówkiem Expect dla odpowiedzi HTTP 100-Continue, po czym zakończy on faktyczne żądanie POST. Wydaje się jednak, że gdy robi to drugie żądanie, nagłówek Authorization nie jest już obecny, a więc atrybut Authorize API spowoduje 401-Nieautoryzowaną odpowiedź, zamiast faktycznie uruchomić metodę API.

Więc, jak wziąć token na okaziciela, który mogę pobrać z interfejsu API i użyć go na kolejnych żądaniach,, w tym różne żądania POST, które będę musiał wykonać?

Co jest najlepszym sposobem na przechowywanie tego tokena w aplikacji MVC? Wolałabym uniknąć omijania łańcucha z każdą metodą w aplikacji, która mogłaby go potrzebować, ale ja też czytałem, że przechowywanie jej w ciasteczku to bardzo zły pomysł ze względów bezpieczeństwa.

kilka dodatkowych punktów, które będą interesujące natychmiast po tym, jak przejdzie ten problem:

Czy przy użyciu protokołu OAuth okaziciela Żetony znaczy, że nie może już używać ApplicationCookies dla aplikacji MVC?I/lub uczyni poniższy kod bezużyteczny w całej aplikacji?

User.Identity.GetUserId()

Obecnie jestem zmuszony do komentowania mój API [Autoryzacja] atrybutów w celu kontynuowania mojej pracy, co oczywiście nie jest idealne, ale to nie pozwalają mi chwilowo zajmować się rzeczami.

pliki startowe:

MVC:

public class Startup 
{ 
    public void Configuration(IAppBuilder app) 
    { 
     ConfigureAuth(app); 
    } 

    private void ConfigureAuth(IAppBuilder app) 
    { 
     app.CreatePerOwinContext(ADUIdentityDbContext.Create); 
     app.CreatePerOwinContext<ADUUserManager>(ADUUserManager.Create); 

     app.UseOAuthBearerTokens(new OAuthAuthorizationServerOptions 
           { 
            AccessTokenExpireTimeSpan = TimeSpan.FromDays(1), 
            //This should be set to FALSE before we move to production. 
            AllowInsecureHttp = true, 
            ApplicationCanDisplayErrors = true, 
            TokenEndpointPath = new PathString("/api/token"), 

           }); 

     app.UseCookieAuthentication(new CookieAuthenticationOptions 
            { 
             AuthenticationType = DefaultAuthenticationTypes.ExternalBearer, 
             CookieName = "ADU", 
             ExpireTimeSpan = TimeSpan.FromHours(2), 
             LoginPath = new PathString("/Account/Login"), 
             SlidingExpiration = true, 

            }); 
    } 
} 

API

public class Startup 
{ 
    public void Configuration(IAppBuilder app) 
    { 
     HttpConfiguration config = new HttpConfiguration(); 

     config.DependencyResolver = new NinjectResolver(new Ninject.Web.Common.Bootstrapper().Kernel); 

     WebApiConfig.Register(config); 

     ConfigureOAuth(app); 
     app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll); 

     app.UseWebApi(config); 
    } 

    public void ConfigureOAuth(IAppBuilder app) 
    { 
     app.CreatePerOwinContext(ADUIdentityDbContext.Create); 
     app.CreatePerOwinContext<ADUUserManager>(ADUUserManager.Create); 

     OAuthAuthorizationServerOptions oAuthServerOptions = new OAuthAuthorizationServerOptions() 
     { 
      AllowInsecureHttp = true, 
      TokenEndpointPath = new PathString("/api/token"), 
      AccessTokenExpireTimeSpan = TimeSpan.FromDays(1), 
      Provider = new SimpleAuthorizationServerProvider(), 
     }; 

     //token generation 
     app.UseOAuthAuthorizationServer(oAuthServerOptions); 
     app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions()); 
    } 
} 


public class SimpleAuthorizationServerProvider : OAuthAuthorizationServerProvider 
{ 
    private IUserBusinessLogic _userBusinessLogic; 

    /// <summary> 
    /// Creates the objects necessary to initialize the user business logic field and initializes it, as this cannot be done by dependency injection in this case. 
    /// </summary> 
    public void CreateBusinessLogic() 
    { 
     IUserRepository userRepo = new UserRepository(); 
     IGeneratedExamRepository examRepo = new GeneratedExamRepository(); 
     IGeneratedExamBusinessLogic examBLL = new GeneratedExamBusinessLogic(examRepo); 
     _userBusinessLogic = new UserBusinessLogic(userRepo, examBLL); 
    } 

    public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context) { context.Validated(); } 

    public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context) 
    { 
     context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { "*" }); 

     //create a claim for the user 
     ClaimsIdentity identity = new ClaimsIdentity(context.Options.AuthenticationType); 
     identity.AddClaim(new Claim("sub", user.Id)); 
     context.Validated(identity); 
    } 
} 

Odpowiedz

1

Po sporo czasu pracy na innych aspektach pro ject, implementacja innych funkcji tak naprawdę ułatwiła rozwiązanie tego problemu - w ramach API istnieje teraz program obsługi wrapperów, a część tego programu obsługi zapisuje wszystkie nagłówki z przychodzących żądań i dodaje je do wychodzących odpowiedzi. Sądzę, że pozwala to stronie ASP.NET MVC aplikacji na wysłanie nagłówka Authorization ponownie po początkowym wysłaniu żądania 200-OK.

zmodyfikowałem moje uwierzytelniania w celu skorzystania ról, ale będę próbować wykluczyć tego kodu, ponieważ nie powinno być istotne tutaj:

MVC Startup.cs:

public class Startup 
{ 
    public void Configuration(IAppBuilder app) { ConfigureAuth(app); } 

    /// <summary> 
    ///  Configures authentication settings for OAuth. 
    /// </summary> 
    /// <param name="app"></param> 
    private void ConfigureAuth(IAppBuilder app) 
    { 
     app.CreatePerOwinContext(ADUIdentityDbContext.Create); 
     app.CreatePerOwinContext<ADUUserManager>(ADUUserManager.Create); 

     app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions()); 

     app.UseCookieAuthentication(new CookieAuthenticationOptions 
            { 
             AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie, 
             CookieName = "ADU", 
             ExpireTimeSpan = TimeSpan.FromHours(2), 
             LoginPath = new PathString("/Account/Login"), 
             SlidingExpiration = true 
            }); 
    } 
} 

Gdzie jest on używany (AccountController):

private async Task CreateLoginCookie(AuthorizationToken response, User result) 
    { 
     //Create the claims needed to log a user in 
     //(uses UserManager several layers down in the stack) 
     ClaimsIdentity cookieIdent = await _clientSDK.CreateClaimsIdentityForUser(response.AccessToken, result, true).ConfigureAwait(false); 

     if (cookieIdent == null) throw new NullReferenceException("Failed to create claims for cookie."); 
     cookieIdent.AddClaim(new Claim("AuthToken", response.AccessToken)); 

     AuthenticationProperties authProperties = new AuthenticationProperties(); 
     authProperties.AllowRefresh = true; 
     authProperties.IsPersistent = true; 
     authProperties.IssuedUtc = DateTime.Now.ToUniversalTime(); 

     IOwinContext context = HttpContext.GetOwinContext(); 
     AuthenticateResult authContext = await context.Authentication.AuthenticateAsync(DefaultAuthenticationTypes.ApplicationCookie); 

     if (authContext != null) 
      context.Authentication.AuthenticationResponseGrant = new AuthenticationResponseGrant(cookieIdent, authContext.Properties); 

     //Wrapper methods for IOwinContext.Authentication.SignOut()/SignIn() 
     SignOut(); 
     SignIn(authProperties, cookieIdent); 
    } 

w moim warstwy SDK, stworzyłem metodę nazywam od różnych innych metod użyć, aby osiągnąć mój API w celu ustalenia Upoważnienie dla każdego żądania wychodzącego (Chciałbym dowiedzieć się, jak zrobić to w atrybucie, ale będę się martwić o tym później):

private void SetAuthentication() 
    { 
     ClaimsIdentity ident = (ClaimsIdentity)Thread.CurrentPrincipal.Identity; 
     Claim claim; 
     //Both of these methods (Thread.CurrentPrincipal, and ClaimsPrincipal.Current should work, 
     //leaving both in for the sake of example. 
     try 
     { 
      claim = ident.Claims.First(x => x.Type == "AuthToken"); 
     } 
     catch (Exception) 
     { 
      claim = ClaimsPrincipal.Current.Claims.First(x => x.Type == "AuthToken"); 
     } 

     _apiClient.SetBearerAuthentication(claim.Value); 
    } 

API Startup.cs

/// <summary> 
    ///  Configures the settings used by the framework on application start. Dependency Resolver, OAuth, Routing, and CORS 
    ///  are configured. 
    /// </summary> 
    /// <param name="app"></param> 
    public void Configuration(IAppBuilder app) 
    { 
     HttpConfiguration config = new HttpConfiguration(); 

     config.DependencyResolver = new NinjectResolver(new Bootstrapper().Kernel); 

     WebApiConfig.Register(config); 

     ConfigureOAuth(app); 
     app.UseCors(CorsOptions.AllowAll); 

     app.UseWebApi(config); 
    } 

    /// <summary> 
    ///  Configures authentication options for OAuth. 
    /// </summary> 
    /// <param name="app"></param> 
    public void ConfigureOAuth(IAppBuilder app) 
    { 
     app.CreatePerOwinContext(ADUIdentityDbContext.Create); 
     app.CreatePerOwinContext<ADUUserManager>(ADUUserManager.Create); 

     OAuthAuthorizationServerOptions oAuthServerOptions = new OAuthAuthorizationServerOptions 
                  { 
                   AllowInsecureHttp = true, 
                   TokenEndpointPath = new PathString("/api/token"), 
                   AccessTokenExpireTimeSpan = TimeSpan.FromDays(1), 
                   Provider = new SimpleAuthorizationServerProvider() 
                  }; 

     //token generation 
     app.UseOAuthAuthorizationServer(oAuthServerOptions); 
     app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions()); 
    } 

SimpleAuthorizationServerProvider .cs: ​​

/// <summary> 
    ///  Creates an access bearer token and applies custom login validation logic to prevent invalid login attempts. 
    /// </summary> 
    /// <param name="context"></param> 
    /// <returns></returns> 
    public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context) 
    { 
     context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { "*" }); 

     // Performs any login logic required, such as accessing Active Directory and password validation. 
     User user = await CustomLoginLogic(context).ConfigureAwait(false); 

     //If a use was not found, add an error if one has not been added yet 
     if((user == null) && !context.HasError) SetInvalidGrantError(context); 

     //Break if any errors have been set. 
     if (context.HasError) return; 

     //create a claim for the user 
     ClaimsIdentity identity = new ClaimsIdentity(context.Options.AuthenticationType); 

     //Add some basic information to the claim that will be used for the token. 
     identity.AddClaim(new Claim("Id", user?.Id)); 
     identity.AddClaim(new Claim("TimeOf", DateTime.Now.ToShortDateString() + " " + DateTime.Now.ToLongTimeString())); 

     //Roles auth 
     SetRoleClaim(user, ref identity); 

     context.Validated(identity); 
    } 

i wreszcie, że kluczem pozorna zawija wszystko razem:

public class ResponseWrappingHandler : DelegatingHandler 
{ 
    /// <summary> 
    /// Catches the request before processing is completed and wraps the resulting response in a consistent response wrapper depending on the response returned by the api. 
    /// </summary> 
    /// <param name="request">The request that is being processed.</param> 
    /// <param name="cancellationToken">A cancellation token to cancel the processing of a request.</param> 
    /// <returns></returns> 
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 
    { 
     HttpResponseMessage response = await base.SendAsync(request, cancellationToken); 

     //Calls Wrapping methods depending on conditions, 
     //All of the Wrapping methods will make a call to PreserveHeaders() 
    } 

    /// <summary> 
    /// Creates a response based on the provided request with the provided response's status code and request headers, and the provided response data. 
    /// </summary> 
    /// <param name="request">The original request.</param> 
    /// <param name="response">The reqsponse that was generated.</param> 
    /// <param name="responseData">The data to include in the wrapped response.</param> 
    /// <returns></returns> 
    private static HttpResponseMessage PreserveHeaders(HttpRequestMessage request, HttpResponseMessage response, object responseData) 
    { 
     HttpResponseMessage newResponse = request.CreateResponse(response.StatusCode, responseData); 

     foreach (KeyValuePair<string, IEnumerable<string>> header in response.Headers) 
      newResponse.Headers.Add(header.Key, header.Value); 

     return newResponse; 
    } 

Dzięki temu projektowi mój projekt może teraz korzystać z autoryzacji/uwierzytelniania bez konieczności posiadania sekretów klienta i takich (co było jednym z celów mojego pracodawcy).