.net6 쿠키 로그인시 같은 도메인에서 쿠키를 여러개 만드는게 가능한가요??

안녕하세요
.net 6에서 쿠키 인증 로그인 기능을 만들어야 하는데 일반 쿠키 인증 로그인과는 방식이 조금 다릅니다.

도메인은 같은데 뒤에 주소에 따라 쿠키를 다르게 저장해야 합니다.
예를들면
abc.co.kr/A/index
abc.co.kr/B/index
abc.co.kr/C/index

이렇게 있으면 A, B, C에 대해 각각 다른 페이지가 호출이 되고, 로그인도 각각 따로 되야합니다.
지금 로그인 할때 HttpContext.SignInAsync 를 통해 쿠키를 생성하도록 했는데 A에서 로그인하면 B랑 C에서도 로그인 상태가 유지가 됩니다.

쿠키 이름을 다르게 줘봐도 여전히 같은 문제가 발생합니다.

위에 나와있는거처럼 같은 도메인에서 뒤에 주소에 따라 쿠키를 각각 다르게 적용이 되도록 가능한가요??

2개의 좋아요

도메인은 같으나 path에 따라 개별 세션 쿠키 생성 방식(multi-tenant scheme)에 대해 테스트를 해 봤는데요.
결론부터 말씀드리면, 되는데 보강이 필요합니다.

우선 코드부터 공유 드릴게요.
테스트 코드라서 보안에 취약한 부분이 있습니다.
운영에 그대로 적용하시면 안되세요!

Cookie Scheme 등록

각 path별 Cookie 인증 Scheme을 등록합니다.

builder.Services.AddAuthentication(o =>
       {
           o.RequireAuthenticatedSignIn = false;
       })
       .AddCookie("ACookie", options => 
       {
           options.ExpireTimeSpan = TimeSpan.FromHours(1);
           options.SlidingExpiration = true;
           options.Cookie.Name = "ACookie";
           options.Cookie.Path = "/A";
           options.CookieManager = new CookieManager();
       })
       .AddCookie("BCookie", options =>
       {
           options.ExpireTimeSpan = TimeSpan.FromHours(1);
           options.SlidingExpiration = true;
           options.Cookie.Name = "BCookie";
           options.Cookie.Path = "/B";
           options.CookieManager = new CookieManager();
       })
       .AddCookie("CCookie", options =>
       {
           options.ExpireTimeSpan = TimeSpan.FromHours(1);
           options.SlidingExpiration = true;
           options.Cookie.Name = "CCookie";
           options.Cookie.Path = "/C";
           options.CookieManager = new CookieManager();
       });

Path별 쿠키를 처리할 CookieManager

path 별로 쿠키를 처리할 CookieManager를 별도 구현합니다.

public class CookieManager : ICookieManager
{
    private readonly ICookieManager ConcreteManager;

    private string RemoveSubdomain(string host)
    {
        var splitHostname = host.Split('.');

        //if not localhost
        if (splitHostname.Length > 1)
        {
            return string.Join(".", splitHostname.Skip(1));
        }
        else
        {
            return host;
        }
    }

    public CookieManager()
    {
        ConcreteManager = new ChunkingCookieManager();
    }

    public void AppendResponseCookie(HttpContext context, string key, string value, CookieOptions options)
    {
        options.Domain = RemoveSubdomain(context.Request.Host.Host); //Set the Cookie Domain using the request from host
        ConcreteManager.AppendResponseCookie(context, key, value, options);
    }

    public void DeleteCookie(HttpContext context, string key, CookieOptions options)
    {
        ConcreteManager.DeleteCookie(context, key, options);
    }

    public string GetRequestCookie(HttpContext context, string key)
    {
        var result = ConcreteManager.GetRequestCookie(context, key);
        return result;
    }
}

웹 요청시 Cookie 처리

Scheme별 쿠키 인증을 처리하여 Identity Context를 저장합니다.

app.Use(async (context, next) =>
{
    var principal = new ClaimsPrincipal();
    var result1 = await context.AuthenticateAsync("ACookie");
    if (result1?.Principal != null)
    {
        principal.AddIdentities(result1.Principal.Identities);
    }

    var result2 = await context.AuthenticateAsync("BCookie");
    if (result2?.Principal != null)
    {
        principal.AddIdentities(result2.Principal.Identities);
    }

    var result3 = await context.AuthenticateAsync("CCookie");
    if (result3?.Principal != null)
    {
        principal.AddIdentities(result3.Principal.Identities);
    }

    context.User = principal;

    await next();
});

쿠키 생성

SignInAsync시 tenant에 맞는 쿠키가 생성 되도록 지정합니다.

[HttpPost("session")]
public async Task Session()
{
    var userPrincipal = GetPrincipal("Roh", "a@a.com", "AUser");

    var options = new AuthenticationProperties
    {
        IsPersistent = true
    };
    await HttpContext.SignInAsync("ACookie", userPrincipal, options).ConfigureAwait(false);
}

테스트

Path별 API 리스트

API 테스트

B의 session 쿠키를 위해 POST API를 호출 합니다.
image

B 세션 쿠키가 생성 되었습니다.
image

현재 버전

기대결과에 맞는 모습을 보여줍니다.

  • 쿠키가 path 별로 생성 됩니다.
  • 같은 Path API 호출시 쿠키가 전달됩니다.
  • 다른 Path API 호출시 쿠키가 전달되지 않습니다.

현재 버전 문제점

[Authorize(AuthenticationSchemes = "CCookie")]
[HttpGet("user")]
public UserResult GetCUser()
{
    return GetUserResult();
}

CCookie를 먼저 생성하고, 위와 같이 특정 API에 인증 제약이 적용되지 않아 API 호출에 실패하는 현상을 보였습니다.
아마도, multi-tenant에 걸맞는 Cookie scheme 세부 처리 옵션이나 AuthorizationHandler를 재작성해서 적용해야 될 것 같은데 너무 깊게 들어가는 것 같아서 현재 수준으로 공유 드립니다.
참조하셔서 원하시는 결과 얻으시길 바래요.

참조

6개의 좋아요

답변 감사합니다.
혹시

.AddCookie("ACookie", options => 
       {
           options.ExpireTimeSpan = TimeSpan.FromHours(1);
           options.SlidingExpiration = true;
           options.Cookie.Name = "ACookie";
           options.Cookie.Path = "/A";
           options.CookieManager = new CookieManager();
       })

에서 .AddCookie(“ACookie”, options => 이 부분중 “ACookie” 이 부분을 하드코딩으로 넣는게 아니라 접속한 url에 따라 자동으로 이름을 지정하도록 가능한가요??

2개의 좋아요

.AddCookie(“ACookie”, options => 이 부분중 “ACookie” 이 부분을 하드코딩으로 넣는게 아니라 접속한 url에 따라 자동으로 이름을 지정하도록 가능한가요??

appsettings.xxx.json 설정 파일이나 환경 변수 값을 정의해서 읽어서 처리하는 것은 가능해 보입니다.
만약 런타임상의 URL 요청에 따라서 설정할 수 있는지를 물어 보시는거라면, 불가능해 보입니다.
그 케이스는 AddCookie 뿐만 아니라, 받아줄 Controller도 동적으로 구성해야 되는거라 전반적으로 문제가 있을 것 같네요.

2개의 좋아요

답변 감사합니다.
알려주신 소스를 변형해서 해봤는데 잘 안되네요 ㅠㅜ;;

builder.Services.AddAuthentication(options =>
{
    //options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    //options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    //options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.RequireAuthenticatedSignIn = false;
}).AddCookie(options =>
{
    options.Cookie.SameSite = SameSiteMode.None;
    options.Events = new CookieAuthenticationEvents
    {
        OnRedirectToLogin = ctx =>
        {
            var RedirectUri = new Uri(ctx.RedirectUri).PathAndQuery;
            var returnUrl = $"/{ctx.Request.RouteValues["domain"]}/{ctx.Request.RouteValues["websettingId"]}/Account/Login?"+ RedirectUri.Split('?')[1];
            options.LoginPath = returnUrl;
            ctx.Response.Redirect(returnUrl);
            return Task.CompletedTask;
        },
        OnSignedIn = ctx =>
        {
            options.Cookie.Name = ctx.Request.RouteValues["domain"].ToString();
            options.Cookie.Path = "/" + ctx.Request.RouteValues["domain"].ToString();
            options.CookieManager = new CookieManager();
            return Task.CompletedTask;
        },
    };
});
app.Use(async (context, next) =>
{
    var principal = new ClaimsPrincipal();
    var result = await context.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
    if (result?.Principal != null)
    {
        principal.AddIdentities(result.Principal.Identities);
    }

    context.User = principal;

    await next();
});

처음에 로그인을 하면 .AspNetCore.Cookies 이 쿠키가 생성이 되면서 쿠키 인증이 적용이 안되다가 두번째 로그인을 하고 나서부터는 정상적으로 동작을 하네요

마지막으로 한가지만 더 질문드려도 될까요??
혹시 처음 웹페이지에 접속할 때 Principal의 Claim을 확인해서 각각 쿠키를 새로 만들거나 유지하는 방법이 있을까요??
예를들면 로그인 시 Principal의 Claim에 url의 일부경로를 저장하고, 웹페이지 접속시 쿠키의 Principal를 확인해서 동일한 Principal이 있으면 쿠키 인증을 유지하고, 없으면 쿠키 인증을 적용 안합니다.
동일 도메인의 다른 url에서 또 로그인하면 새로운 쿠키를 생성해서 각각의 url마다 쿠키 인증을 따로 적용하는 방식입니다.

context.Principal.GetClaimValue(url일부경로) 을 통해 값 확인 후 현재 접속한 url과 비교해서 쿠키 인증을 유지할지 안할지 적용하면 될거 같은데
실제로 가능한지 여부도 모르고 어떻게 구현해야할지를 모르겠네요 ㅠㅜ;;

1개의 좋아요

공유해주신 코드만 봐서는 이유를 잘 모르겠네요. :disappointed_relieved:

테넌트별 개별 쿠키가 아닌 어플리케이션 전체에 사용할 세션 쿠키를 만들고, 웹 요청 URI와 쿠키에 기록된 URI를 비교하는 방식을 말씀하시는 것 같은데요.
AuthorizationHandler를 구현하여 연결하면 되지 않을까 싶네요.
자세한 구현 방법은 MS 가이드를 확인해 주세요.

인증 검사시 사용자 정의한 AuthorizationHandler가 적용되도록 구성합니다.

services.AddAuthorization(o =>
{
    o.DefaultPolicy = new AuthorizationPolicyBuilder()
                        .RequireAuthenticatedUser()
                        .AddAuthenticationSchemes(CookieAuthenticationDefaults.AuthenticationScheme)
                        .AddRequirements(new TenantRequirement())
                        .Build();
});
services.AddScoped<IAuthorizationHandler, TenantAuthorizationHandler>();

요구 사항 정의

public class TenantRequirement : IAuthorizationRequirement
{
}

인증 핸들러 정의

public class TenantAuthorizationHandler : AuthorizationHandler<TenantRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, TenantRequirement requirement)
    {
        if (context is null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        if (requirement is null)
        {
            throw new ArgumentNullException(nameof(requirement));
        }

        // context.User.Identity의 Tenant 값과 Request Path를 비교하여 인증 처리합니다.
        // 실패
        context.Fail(new AuthorizationFailureReason(this, result.ErrorMessage));
        // 성공
        context.Succeed(requirement);
        return Task.CompletedTask;
    }
}

TenantAuthorizationHandler 에서 요청 URI와 Claim을 비교하여 처리하시면 되지 않을까 싶네요.


여러 테넌트를 처리하는 다른 방법으로는 완전 독립형 multi tenant 어플리케이션을 구성하는건데요.
처음 질문때 말씀하신 각 tenant별 쿠키를 발급하고, tenant별 인증 처리, 상태 관리, API를 구성합니다.
tenant간 간섭이 없는 구조로 다른 tenant를 침범하는 불상사가 적은 방식이라고 할 수 있는데요.
개발에 정답은 없습니다만, 정석적인 방법이라고 할 수 있습니다. :open_book:

직접 구현보다는 잘 만들어진 패키지를 이용하시는 편이 시행착오가 적을 것이라 관련 패키지 공유 드려요.
https://www.nuget.org/packages?q=Finbuckle.MultiTenant
구현, 유지보수 비용이 큰 방식이라 신중하게 검토하시는 것을 권장 드려요. :thinking:
여러 옵션에 대해 검토 해 보시고, 좋은 결과 얻으시길~! :clinking_glasses:

3개의 좋아요

답변 감사합니다.
이 부분은 시간이 좀 걸릴거 같아서 일단 다른 부분 먼저 진행하고 나중에 다시 한번 들여다봐야 할거 같네요 ㅠㅜ;
보내주신 내용 참고해서 진행해보도록 하겠습니다.
답변 정말 감사합니다 !!

2개의 좋아요