Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions ydb/mvp/oidc_proxy/context.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
#include <util/generic/string.h>
#include <util/random/random.h>
#include <util/string/builder.h>
#include <library/cpp/string_utils/base64/base64.h>
#include <ydb/library/actors/http/http.h>
#include "openid_connect.h"
#include "context.h"

namespace NMVP {
namespace NOIDC {

TContext::TContext(const TString& state, const TString& requestedAddress, bool isAjaxRequest)
: State(state)
, AjaxRequest(isAjaxRequest)
, RequestedAddress(requestedAddress)
{}

TContext::TContext(const NHttp::THttpIncomingRequestPtr& request)
: State(GenerateState())
, AjaxRequest(DetectAjaxRequest(request))
, RequestedAddress(GetRequestedUrl(request, AjaxRequest))
{}

TString TContext::GetState() const {
return State;
}

bool TContext::IsAjaxRequest() const {
return AjaxRequest;
}

TString TContext::GetRequestedAddress() const {
return RequestedAddress;
}

TString TContext::CreateYdbOidcCookie(const TString& secret) const {
static constexpr size_t COOKIE_MAX_AGE_SEC = 420;
return TStringBuilder() << CreateNameYdbOidcCookie(secret, State) << "="
<< GenerateCookie(secret) << ";"
" Path=" << GetAuthCallbackUrl() << ";"
" Max-Age=" << COOKIE_MAX_AGE_SEC << ";"
" SameSite=None; Secure";
}

TString TContext::GenerateCookie(const TString& secret) const {
const TDuration StateLifeTime = TDuration::Minutes(10);
TInstant expirationTime = TInstant::Now() + StateLifeTime;
TStringBuilder stateStruct;
stateStruct << "{\"state\":\"" << State
<< "\",\"requested_address\":\"" << RequestedAddress
<< "\",\"expiration_time\":" << ToString(expirationTime.TimeT())
<< ",\"ajax_request\":" << (AjaxRequest ? "true" : "false") << "}";
TString digest = HmacSHA256(secret, stateStruct);
TString cookieStruct {"{\"state_struct\":\"" + Base64Encode(stateStruct) + "\",\"digest\":\"" + Base64Encode(digest) + "\"}"};
return Base64Encode(cookieStruct);
}

TString TContext::GenerateState() {
TStringBuilder sb;
static constexpr size_t CHAR_NUMBER = 15;
for (size_t i{0}; i < CHAR_NUMBER; i++) {
sb << RandomNumber<char>();
}
return Base64EncodeUrlNoPadding(sb);
}

bool TContext::DetectAjaxRequest(const NHttp::THttpIncomingRequestPtr& request) {
static const THashMap<TStringBuf, TStringBuf> expectedHeaders {
{"Accept", "application/json"}
};
NHttp::THeaders headers(request->Headers);
for (const auto& el : expectedHeaders) {
TStringBuf headerValue = headers.Get(el.first);
if (!headerValue || headerValue.find(el.second) == TStringBuf::npos) {
return false;
}
}
return true;
}

TStringBuf TContext::GetRequestedUrl(const NHttp::THttpIncomingRequestPtr& request, bool isAjaxRequest) {
NHttp::THeaders headers(request->Headers);
TStringBuf requestedUrl = headers.Get("Referer");
if (!isAjaxRequest || requestedUrl.empty()) {
return request->URL;
}
return requestedUrl;
}

} // NOIDC
} // NMVP
41 changes: 41 additions & 0 deletions ydb/mvp/oidc_proxy/context.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#pragma once

#include <util/generic/string.h>
#include <util/generic/ptr.h>

namespace NHttp {

class THttpIncomingRequest;
using THttpIncomingRequestPtr = TIntrusivePtr<THttpIncomingRequest>;

}

namespace NMVP {
namespace NOIDC {

class TContext {
private:
TString State;
bool AjaxRequest = false;
TString RequestedAddress;

public:
TContext(const TString& state = "", const TString& requestedAddress = "", bool isAjaxRequest = false);
TContext(const NHttp::THttpIncomingRequestPtr& request);

TString GetState() const;
bool IsAjaxRequest() const;
TString GetRequestedAddress() const;

TString CreateYdbOidcCookie(const TString& secret) const;

private:
static TString GenerateState();
static bool DetectAjaxRequest(const NHttp::THttpIncomingRequestPtr& request);
static TStringBuf GetRequestedUrl(const NHttp::THttpIncomingRequestPtr& request, bool isAjaxRequest);

TString GenerateCookie(const TString& secret) const;
};

} // NOIDC
} // NMVP
1 change: 0 additions & 1 deletion ydb/mvp/oidc_proxy/oidc_protected_page.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ void THandlerSessionServiceCheck::Bootstrap(const NActors::TActorContext& ctx) {
return;
}
NHttp::THeaders headers(Request->Headers);
IsAjaxRequest = DetectAjaxRequest(headers);
TStringBuf authHeader = headers.Get(AUTH_HEADER_NAME);
if (Request->Method == "OPTIONS" || IsAuthorizedRequest(authHeader)) {
ForwardUserRequest(TString(authHeader), ctx);
Expand Down
1 change: 0 additions & 1 deletion ydb/mvp/oidc_proxy/oidc_protected_page.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ class THandlerSessionServiceCheck : public NActors::TActorBootstrapped<THandlerS
const TOpenIdConnectSettings Settings;
TString ProtectedPageUrl;
TString RequestedPageScheme;
bool IsAjaxRequest = false;

const static inline TStringBuf IAM_TOKEN_SCHEME = "Bearer ";
const static inline TStringBuf IAM_TOKEN_SCHEME_LOWER = "bearer ";
Expand Down
4 changes: 3 additions & 1 deletion ydb/mvp/oidc_proxy/oidc_protected_page_nebius.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include <ydb/mvp/core/mvp_tokens.h>
#include <ydb/mvp/core/mvp_log.h>
#include "openid_connect.h"
#include "context.h"
#include "oidc_protected_page_nebius.h"

namespace NMVP {
Expand Down Expand Up @@ -99,7 +100,8 @@ void THandlerSessionServiceCheckNebius::ExchangeSessionToken(const TString sessi

void THandlerSessionServiceCheckNebius::RequestAuthorizationCode(const NActors::TActorContext& ctx) {
LOG_DEBUG_S(ctx, EService::MVP, "Request authorization code");
NHttp::THttpOutgoingResponsePtr httpResponse = GetHttpOutgoingResponsePtr(Request, Settings, IsAjaxRequest);
TContext context(Request);
NHttp::THttpOutgoingResponsePtr httpResponse = GetHttpOutgoingResponsePtr(Request, Settings, context);
ctx.Send(Sender, new NHttp::TEvHttpProxy::TEvHttpOutgoingResponse(httpResponse));
Die(ctx);
}
Expand Down
4 changes: 3 additions & 1 deletion ydb/mvp/oidc_proxy/oidc_protected_page_yandex.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include <ydb/mvp/core/mvp_tokens.h>
#include <ydb/mvp/core/appdata.h>
#include <ydb/mvp/core/mvp_log.h>
#include "context.h"
#include "oidc_protected_page_yandex.h"

namespace NMVP {
Expand Down Expand Up @@ -31,7 +32,8 @@ void THandlerSessionServiceCheckYandex::Handle(TEvPrivate::TEvErrorResponse::TPt
LOG_DEBUG_S(ctx, EService::MVP, "SessionService.Check(): " << event->Get()->Status);
NHttp::THttpOutgoingResponsePtr httpResponse;
if (event->Get()->Status == "400") {
httpResponse = GetHttpOutgoingResponsePtr(Request, Settings, IsAjaxRequest);
TContext context(Request);
httpResponse = GetHttpOutgoingResponsePtr(Request, Settings, context);
} else {
httpResponse = Request->CreateResponse( event->Get()->Status, event->Get()->Message, "text/plain", event->Get()->Details);
}
Expand Down
58 changes: 22 additions & 36 deletions ydb/mvp/oidc_proxy/oidc_proxy_ut.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include "oidc_session_create_handler.h"
#include "oidc_settings.h"
#include "openid_connect.h"
#include "context.h"

using namespace NMVP::NOIDC;

Expand Down Expand Up @@ -719,23 +720,18 @@ Y_UNIT_TEST_SUITE(Mvp) {
TStringBuilder request;
request << "GET /auth/callback?code=code_template&state=" << state << " HTTP/1.1\r\n";
request << "Host: " + hostProxy + "\r\n";
request << "Cookie: " << CreateNameYdbOidcCookie(settings.ClientSecret, wrongState) << "=" << GenerateCookie(wrongState, "/requested/page", settings.ClientSecret, redirectStrategy.IsAjaxRequest()) << "\r\n";
TContext context(wrongState, "/requested/page", redirectStrategy.IsAjaxRequest());
request << "Cookie: " << context.CreateYdbOidcCookie(settings.ClientSecret) << "\r\n";
NHttp::THttpIncomingRequestPtr incomingRequest = new NHttp::THttpIncomingRequest();
EatWholeString(incomingRequest, redirectStrategy.CreateRequest(request));
incomingRequest->Endpoint->Secure = true;
runtime.Send(new IEventHandle(sessionCreator, edge, new NHttp::TEvHttpProxy::TEvHttpIncomingRequest(incomingRequest)));

TAutoPtr<IEventHandle> handle;
NHttp::TEvHttpProxy::TEvHttpOutgoingResponse* outgoingResponseEv = runtime.GrabEdgeEvent<NHttp::TEvHttpProxy::TEvHttpOutgoingResponse>(handle);
UNIT_ASSERT_STRINGS_EQUAL(outgoingResponseEv->Response->Status, "302");
const NHttp::THeaders headers(outgoingResponseEv->Response->Headers);
UNIT_ASSERT(headers.Has("Location"));
TString location = TString(headers.Get("Location"));
UNIT_ASSERT_STRING_CONTAINS(location, "https://auth.test.net/oauth/authorize");
UNIT_ASSERT_STRING_CONTAINS(location, "response_type=code");
UNIT_ASSERT_STRING_CONTAINS(location, "scope=openid");
UNIT_ASSERT_STRING_CONTAINS(location, "client_id=" + settings.ClientId);
UNIT_ASSERT_STRING_CONTAINS(location, "redirect_uri=https://" + hostProxy + "/auth/callback");
UNIT_ASSERT_STRINGS_EQUAL(outgoingResponseEv->Response->Status, "400");
UNIT_ASSERT_STRING_CONTAINS(outgoingResponseEv->Response->Body, "Unknown error has occurred. Please open the page again");

}

Y_UNIT_TEST(OpenIdConnectotWrongStateAuthorizationFlow) {
Expand Down Expand Up @@ -773,8 +769,8 @@ Y_UNIT_TEST_SUITE(Mvp) {
TStringBuilder request;
request << "GET /auth/callback?code=code_template&state=" << state << " HTTP/1.1\r\n";
request << "Host: oidcproxy.net\r\n";
const TString oidcCookie = CreateNameYdbOidcCookie(settings.ClientSecret, state);
request << "Cookie: " << oidcCookie << "=" << GenerateCookie(state, "/requested/page", settings.ClientSecret, false) << "\r\n\r\n";
TContext context(state, "/requested/page", false);
request << "Cookie: " << context.CreateYdbOidcCookie(settings.ClientSecret) << "\r\n\r\n";
NHttp::THttpIncomingRequestPtr incomingRequest = new NHttp::THttpIncomingRequest();
EatWholeString(incomingRequest, request);
runtime.Send(new IEventHandle(sessionCreator, edge, new NHttp::TEvHttpProxy::TEvHttpIncomingRequest(incomingRequest)));
Expand Down Expand Up @@ -823,7 +819,8 @@ Y_UNIT_TEST_SUITE(Mvp) {
TStringBuilder request;
request << "GET /auth/callback?code=code_template&state=" << state << " HTTP/1.1\r\n";
request << "Host: oidcproxy.net\r\n";
request << "Cookie: " << CreateNameYdbOidcCookie(settings.ClientSecret, state) << "=" << GenerateCookie(state, "/requested/page", settings.ClientSecret, redirectStrategy.IsAjaxRequest()) << "\r\n";
TContext context(state, "/requested/page", redirectStrategy.IsAjaxRequest());
request << "Cookie: " << context.CreateYdbOidcCookie(settings.ClientSecret) << "\r\n";
NHttp::THttpIncomingRequestPtr incomingRequest = new NHttp::THttpIncomingRequest();
EatWholeString(incomingRequest, redirectStrategy.CreateRequest(request));
incomingRequest->Endpoint->Secure = true;
Expand All @@ -843,22 +840,11 @@ Y_UNIT_TEST_SUITE(Mvp) {
"Content-Length: " + ToString(authorizationServerResponse.length()) + "\r\n\r\n" + authorizationServerResponse);
runtime.Send(new IEventHandle(handle->Sender, edge, new NHttp::TEvHttpProxy::TEvHttpIncomingResponse(outgoingRequestEv->Request, incomingResponse)));
auto outgoingResponseEv = runtime.GrabEdgeEvent<NHttp::TEvHttpProxy::TEvHttpOutgoingResponse>(handle);
redirectStrategy.CheckRedirectStatus(outgoingResponseEv);
TString location = redirectStrategy.GetRedirectUrl(outgoingResponseEv);
UNIT_ASSERT_STRING_CONTAINS(location, "https://auth.test.net/oauth/authorize");
UNIT_ASSERT_STRING_CONTAINS(location, "response_type=code");
UNIT_ASSERT_STRING_CONTAINS(location, "scope=openid");
UNIT_ASSERT_STRING_CONTAINS(location, "client_id=" + settings.ClientId);
UNIT_ASSERT_STRING_CONTAINS(location, "redirect_uri=https://oidcproxy.net/auth/callback");

NHttp::TUrlParameters urlParameters(location);
const TString newState = urlParameters["state"];

NHttp::THeaders headers(outgoingResponseEv->Response->Headers);
UNIT_ASSERT(headers.Has("Set-Cookie"));
const TStringBuf setCookie = headers.Get("Set-Cookie");
UNIT_ASSERT_STRING_CONTAINS(setCookie, CreateNameYdbOidcCookie(settings.ClientSecret, newState));
redirectStrategy.CheckSpecificHeaders(headers);
UNIT_ASSERT_STRINGS_EQUAL(outgoingResponseEv->Response->Status, "302");
const NHttp::THeaders headers(outgoingResponseEv->Response->Headers);
UNIT_ASSERT(headers.Has("Location"));
TStringBuf location = headers.Get("Location");
UNIT_ASSERT_STRING_CONTAINS(location, "/requested/page");
}

Y_UNIT_TEST(OpenIdConnectSessionServiceCreateAccessTokenInvalid) {
Expand Down Expand Up @@ -896,8 +882,8 @@ Y_UNIT_TEST_SUITE(Mvp) {
TStringBuilder request;
request << "GET /callback?code=code_template&state=" << state << " HTTP/1.1\r\n";
request << "Host: oidcproxy.net\r\n";
const TString oidcCookie = CreateNameYdbOidcCookie(settings.ClientSecret, state);
request << "Cookie: " << oidcCookie << "=" << GenerateCookie(state, "/requested/page", settings.ClientSecret, false) << "\r\n\r\n";
TContext context(state, "/requested/page", false);
request << "Cookie: " << context.CreateYdbOidcCookie(settings.ClientSecret) << "\r\n\r\n";
NHttp::THttpIncomingRequestPtr incomingRequest = new NHttp::THttpIncomingRequest();
EatWholeString(incomingRequest, request);
runtime.Send(new IEventHandle(sessionCreator, edge, new NHttp::TEvHttpProxy::TEvHttpIncomingRequest(incomingRequest)));
Expand Down Expand Up @@ -941,14 +927,14 @@ Y_UNIT_TEST_SUITE(Mvp) {
std::unique_ptr<grpc::Server> sessionServer(builder.BuildAndStart());

const NActors::TActorId sessionCreator = runtime.Register(new TSessionCreateHandler(edge, settings));
TStringBuf firstRequestState = "first_request_state";
TStringBuf secondRequestState = "second_request_state";
TString firstCookie {CreateNameYdbOidcCookie(settings.ClientSecret, firstRequestState) + "=" + GenerateCookie(firstRequestState, "/requested/page", settings.ClientSecret, redirectStrategy.IsAjaxRequest())};
TString secondCookie {CreateNameYdbOidcCookie(settings.ClientSecret, secondRequestState) + "=" + GenerateCookie(secondRequestState, "/requested/page", settings.ClientSecret, redirectStrategy.IsAjaxRequest())};
TString firstRequestState = "first_request_state";
TString secondRequestState = "second_request_state";
TContext context1(firstRequestState, "/requested/page", redirectStrategy.IsAjaxRequest());
TContext context2(secondRequestState, "/requested/page", redirectStrategy.IsAjaxRequest());
TStringBuilder request;
request << "GET /auth/callback?code=code_template&state=" << firstRequestState << " HTTP/1.1\r\n";
request << "Host: oidcproxy.net\r\n";
request << "Cookie: " << firstCookie << "; " << secondCookie << "\r\n";
request << "Cookie: " << context1.CreateYdbOidcCookie(settings.ClientSecret) << "; " << context2.CreateYdbOidcCookie(settings.ClientSecret) << "\r\n";
NHttp::THttpIncomingRequestPtr incomingRequest = new NHttp::THttpIncomingRequest();
EatWholeString(incomingRequest, redirectStrategy.CreateRequest(request));
incomingRequest->Endpoint->Secure = true;
Expand Down
Loading