avatarYaz

Summary

The provided content outlines an approach to implementing security in ASP.NET Core Web APIs using Onion Architecture, emphasizing the use of a SessionContext as a cross-cutting concern to manage authentication and user context across different layers.

Abstract

The article discusses the integration of security measures within the Onion Architecture framework for ASP.NET Core Web APIs. It demonstrates how to use a SessionContext to handle user authentication and session management consistently across the architecture's layers, from the controller to the repository. The SessionContext is designed as a cross-cutting concern, allowing it to be accessed throughout the application without tightly coupling it to the business logic. The implementation includes methods for authenticating users, managing session expiration, and associating user information with application data. The article also suggests further enhancements, such as adding granular permissions and using IAuthorizationFilter for declarative security. The author encourages readers to follow them for more content and to support their work by buying them a cup of coffee.

Opinions

  • The author believes that security should be a primary concern in application development and that Onion Architecture provides a robust framework for its implementation.
  • The use of SessionContext as a cross-cutting concern is presented as a best practice for maintaining clean and decoupled service layers.
  • The author values the separation of concerns and suggests that implementing security in this manner leads to more maintainable and testable code.
  • There is an emphasis on the flexibility of the SessionContext implementation, with the author noting that developers can choose their own strategy for persisting session information, whether through cookies, claim providers, or other means.
  • The article implies that the approach described is part of a larger set of best practices, with a reference to a previous blog post on the Repository Pattern and a suggestion to explore permission domain models and declarative security filters.
  • The author is open to community engagement and support, indicating a willingness to continue producing helpful content for the developer community.

Implementing Security in Onion Architecture — ASP.NET Core 8.0

Security is an integral part of any application. In Onion Architecture, security is implemented as a Cross-cutting Concern. Let us see how we can implement security in ASP.NET Core Web APIs by observing Onion Architecture.

Photo by Tangerine Newt on Unsplash

Let us consider the following code snippet:

// Controller - Onion Architecture: Interface Layer
public class UserProfileController : ControllerBase
{
  private readonly ISessionContext _sessionContext;
  private readonly IApplicationService _applicationService;

  public UserProfileController(
    ISessionContext sessionContext
    IApplicationService applicationService)
  {
    this._sessionContext = sessionContext;
    this._applicationService = applicationService;
  }

  // Sample save application web api
  [HttpGet]
  public ActionResult<bool> SaveApplication(
    Guid applicationId,
    string applicantName)
  {
    var result = false;
    // Is user autenticated?
    var authenticated = this._sessionContext.IsAuthenticated();

    if (authenticated) {
      // If user is autneticated then save application
      result = this._applicationService
        .SaveApplication(applicationId, applicantName);
    }

    return this.Ok(result);
  }
}

// Repository - Onion Architecture: Domain Services Layer
public class ApplicationRepository: IApplicationRepository
{
  private readonly MyDbContext _dbContext;
  private readonly ISessionContext _sessionContext;

  public ApplicationRepository(
    MyDbContext dbContext, 
    ISessionContext sessionContext)
  {
    this._dbContext = dbContext;
    this._sessionContext = sessionContext;
  }

  // Sample Repository call.
  public ApplicationData SaveApplication(
    Guid applicationId,
    string applicantName)
  {
    var userContext = this._sessionContext.User;

    // Save application to database
    return new SaveApplication()
    {
      ApplicationId = applicationId,
      ApplicantName = applicantName,

      // User specific data
      UpdatedBy = userContext.UserId,
      UpdatedOn = userContext.LocalDateTime
    }.Execute(this.DbContext);
  }
}

In the above code, we have implemented Security as a Cross-cutting concern SessionContext, available at all the Onion Architecture layers.

This enables us to not only secure our WebAPIs but also helps us to uncouple current user information from the business logic making the Service Layer cleaner.

Read my blog Implementing Unit Of Work Pattern Using Entity Framework 8.0 — The Ultimate Guide to understand Repository Pattern used above.

Now, let us look at how the SessionContext is constructed!

Below is a simple implementation of SessionContext:

// Session context that is shareable accross onion architecture layers
public class SessionContext : ISessionContext
{
  public readonly ISessionContextAccessor _sessionContextAccessor;
  public readonly UserContext _userContext;

  public SessionContext(ISessionContextAccessor sessionContextAccessor)
  {
    this._sessionContextAccessor = sessionContextAccessor;
  }

  public User UserContext
  { 
    get
    {
      return this._sessionContextAccessor.GetUserContext();
    }
  }

  public bool IsAuthenticated()
  {
    return this._sessionContextAccessor.IsAuthenticated();
  }

  public string SessionId
  {
    get
    {
      return this._sessionContextAccessor.SessionId;
    }
  }

  public bool ValidateSession(int slideMinutes)
  {
    return this._sessionContextAccessor
      .ValidateSession(this.SessionId, slideMinutes);
  }

  public void SlideSession(int slideMinutes)
  {
    this._sessionContextAccessor
      .SlideSession(this.SessionId, slideMinutes);
  }

  public void ExpireSession()
  {
    this._sessionContextAccessor.ExpireSession(this.SessionId);
  }

  public DateTime? GetSessionExpiration() {
    return this._sessionContextAccessor.GetSessionExpiration(this.SessionId);
  }
}

As you can see, SessionContext is just a Delegation to ISessionContextAccessor.

This is because SessionContext is a Cross-cutting concern and must reside at the lowest level so that it can be referenced by all the Onion Architecture layers, whereas we can Implement ISessionContextAccessor at the highest layer which is the Interface/Controller layer.

Let us now see, how ISessionContextAccessor is implemented:

public class SessionContextAccessor : ISessionContextAccessor
{
  private readonly IHttpContextAccessor _httpContextAccessor;
  private readonly ISessionContextService _sessionContextService;

  public SessionContextAccessor(
    IHttpContextAccessor httpContextAccessor,
    ISessionContextService sessionContextService)
  {
    this._httpContextAccessor = httpContextAccessor;
    this._sessionContextService = sessionContextService;
  }

  public string SessionId
  {
    get
    {
      // Implement your own way to persist session information.
      return this._httpContextAccessor
        .HttpContext.Request.Cookies["sessionId"].Value;
    }
  }

  public UserContext GetUserContext()
  {
    var userModel = this._sessionContextService
      .GetUserBySessionId(this.sessionId);
    
    if (userModel != null)
    {
      return new UserContext
      {
      UserId = userModel.UserId,
      FirstName = userModel.FirstName,
      LastName = userModel.LastName,
      EmailAddress = userModel.EmailAddress,
      PhoneNumber = userModel.PhoneNumber
      };
    }
    
    return null;
  }

  public bool ValidateSession(int slideMinutes)
  {
    return this._sessionContextService
      .ValidateSession(this.sessionId, slideMinutes);
  }

  public void SlideSession(int slideMinutes)
  {
    this._sessionContextService
      .SlideSession(this.sessionId, slideMinutes);
  }

  public void ExpireSession()
  {
    this._sessionContextService
      .ExpireSession(this.sessionId);
  }

  public bool IsAuthenticated()
  {
    return this._sessionContextService
      .IsAuthenticated(this.sessionId);
  }

  public DateTime? GetSessionExpiration()
  {
    return this._sessionContextService
      .GetUserSessionExpiration(this.sessionId);
  }
}

Above, SessionContextAccessor is again a delegation of a custom SessionContextService service class where we are accessing Session ID through a cookie. Feel free to change the strategy to persist session information yourself whether it be a Claim Provider or any other means.

That is it!

we have fully baked security into our Web APIs.

Where to go from here?

we can extend the Session Context to add security at a granular level such as building permission domain models and using them in the context above.

We can also, implement IAuthorizationFilter to have declarative security to decorate Web APIs.

🔔 Follow me for more content like this! 🔔

Did this article help you today? Buy me a cup of coffee☕ to keep me going!

Aspnetcore
Webapi
Security
Onionarchitecture
Programming
Recommended from ReadMedium