The callback handler is implemented as a hidden endpoint in our API. The handler performs the following tasks:
Parse the form data sent by Microsoft Entra.
std::string code{};
std::string state{};
auto pairs = spt::util::split<std::string_view>( payload, 3, "&" );
for ( auto& pair : pairs )
{
auto parts = spt::util::split<std::string_view>( pair, 2, "=" );
if ( parts.size() != 2 ) continue;
if ( parts[0] == "code" ) code = parts[1];
else if ( parts[0] == "state" )
{
const auto decoded = boost::urls::decode_view( parts[1] );
state = std::string{ decoded.begin(), decoded.end() };
}
}
if ( state.empty() || code.empty() )
{
LOG_WARN << "Missing state or code parameter. APM id: " << apm.id;
return error( 400, "Missing parameters"sv, nullptr, methods, req.header, apm );
}
Retrieve the OIDC information record from the database.
std::expected<model::OIDCInformation, std::string> retrieveOidc( const std::string& value, spt::ilp::APMRecord& apm )
{
using O = std::expected<model::OIDCInformation, std::string>;
auto& cp = spt::ilp::addProcess( apm, spt::ilp::APMRecord::Process::Type::Function );
cp.values.try_emplace( model::ilp::name::APM_NOTE_VALUE, "Retrieve OIDC information" );
DEFER( spt::ilp::setDuration( cp ) );
auto idx = apm.processes.size();
auto cstate = boost::algorithm::replace_all_copy( value, ":", "+" );
auto id = util::Configuration::instance().decrypt( cstate );
auto oid = spt::util::parseId( id );
if ( !oid )
{
LOG_INFO << "Invalid state parameter " << id << ". APM id: " << apm.id;
return O{ std::unexpect, "Invalid state parameter" };
}
WRAP_CODE_LINE( auto [status, oidc] = db::retrieve<model::OIDCInformation>( *oid, ""sv, apm ); )
if ( status != 200 || !oidc )
{
LOG_WARN << "Error retrieving OIDC information for oid " << *oid << ". APM id: " << apm.id;
cp.values.try_emplace( model::ilp::name::APM_ERROR_VALUE, "Error retrieving OIDC information" );
return O{ std::unexpect, "Error retrieving OIDC information" };
}
return O{ std::in_place, std::move( *oidc ) };
}
auto oidc = retrieveOidc( state, apm );
if ( !oidc.has_value() ) return error( 417, oidc.error(), nullptr, methods, req.header, apm );
Retrieve IDP configuration from configuration database.
template <StringLiteral idp>
struct IdpHolder
{
static const IdpHolder& instance()
{
static IdpHolder holder;
return holder;
}
std::string clientId;
std::string clientSecret;
std::string tokenEndpoint;
std::string userInfo;
std::string redirectUri;
std::string success;
private:
IdpHolder()
{
const auto keys = std::vector{ std::format( "/service/sso/oidc/{}/clientId", idp.value ),
std::format( "/service/sso/oidc/{}/clientSecret", idp.value ),
std::format( "/service/sso/oidc/{}/token", idp.value ),
std::format( "/service/sso/oidc/{}/userInfo", idp.value ),
std::format( "/service/sso/oidc/{}/callback", idp.value ),
std::format( "/service/sso/oidc/{}/success", idp.value )
};
const auto svkeys = keys | std::views::transform( []( auto& k ) { return std::string_view{ k }; } ) | std::ranges::to<std::vector>();
const auto results = util::Configuration::instance().get( svkeys );
if ( results.size() != keys.size() )
{
LOG_CRIT << "Error retrieving OIDC configuration";
throw std::runtime_error{ "OIDC configuration error" };
}
clientId = results[0].value_or( ""s );
clientSecret = util::Configuration::instance().decrypt( results[1].value_or( ""s ) );
tokenEndpoint = results[2].value_or( ""s );
userInfo = results[3].value_or( ""s );
redirectUri = results[4].value_or( ""s );
success = results[5].value_or( ""s );
}
};
Model that represents the tokens retrieved from the IDP.
struct OIDCTokens
{
BEGIN_VISITABLES(OIDCTokens);
VISITABLE(std::string, access_token);
VISITABLE(std::string, scope);
VISITABLE(std::string, token_type);
VISITABLE(std::string, id_token);
VISITABLE(std::string, refresh_token);
VISITABLE_DIRECT_INIT(int64_t, expires_in, {0});
END_VISITABLES;
};
Exchange the code returned by Entra for tokens.
const auto& holder = IdpHolder<"exelon">::instance();
auto response = cpr::Post( cpr::Url{ holder.tokenEndpoint },
cpr::Payload{ { "client_id", holder.clientId },
{ "scope", "openid email profile" },
{ "code", code },
{ "redirect_uri", holder.redirectUri },
{ "grant_type", "authorization_code" },
{ "client_secret", holder.clientSecret } } );
if ( response.status_code != 200 )
{
LOG_WARN << "Error retrieving OIDC token. Status code: " << int(response.status_code) << ". " << response.text << ". APM id: " << apm.id;
cp.values.try_emplace( model::ilp::name::APM_ERROR_VALUE, "Error retrieving OIDC token" );
return error( 417, "Error retrieving OIDC token"sv, nullptr, methods, req.header, apm );
}
auto tokens = spt::util::json::unmarshall<OIDCTokens>( response.text );
The JWT token returned by Entra does not include the user's given or family names. Hence, we need to use the *userInfo* endpoint to retrieve the user's name.
const auto response = cpr::Get( cpr::Url{ holder.userInfo }, cpr::Bearer{ tokens.access_token } );
if ( response.status_code != 200 )
{
LOG_WARN << "Error retrieving user info. Status code: " << int(response.status_code) << ". " << response.text << ". APM id: " << apm.id;
cp.values.try_emplace( model::ilp::name::APM_ERROR_VALUE, "Error retrieving OIDC token" );
return error( 417, "Error retrieving user info"sv, nullptr, methods, req.header, apm );
}
auto model = spt::util::json::unmarshall<EntraUser>( response.text );
model.username = claims.preferred_username;
Create/update the platform user as appropriate. If the account initially existed as an internal user, we remove the `password` to ensure the user can only login via the external IDP.
WRAP_CODE_LINE( auto [_, user] = db::retrieve<model::User>( "email", model.email, apm, true ); )
if ( !user )
{
WRAP_CODE_LINE( auto u = createUser( model, *oidc, apm ); )
if ( !u.has_value() ) return error( 417, u.error(), nullptr, methods, req.header, apm );
user.emplace( std::move( u.value() ) );
}
else
{
LOG_INFO << "User " << model.email << " already exists. APM id: " << apm.id;
WRAP_CODE_LINE( removePassword( *user, apm ); )
}
Generate a JWT for the user. This will be used by all our applications to interact with our API.
std::expected<model::Token, std::string> createToken( const model::User& user, model::OIDCInformation& oidc, spt::ilp::APMRecord& apm )
{
using O = std::expected<model::Token, std::string>;
auto& cp = spt::ilp::addProcess( apm, spt::ilp::APMRecord::Process::Type::Function );
cp.values.try_emplace( model::ilp::name::APM_NOTE_VALUE, "Create JWT Token" );
DEFER( spt::ilp::setDuration( cp ) );
auto idx = apm.processes.size();
auto token = util::generateToken( user, std::chrono::duration_cast<std::chrono::minutes>( std::chrono::hours{ 24 } ), model::JwtToken::Type::idp );
WRAP_CODE_LINE( const auto tstatus = db::create( token, apm, true ); )
if ( tstatus != 200 )
{
LOG_WARN << "Error creating token for user " << user.username << ". APM id: " << apm.id;
cp.values.try_emplace( model::ilp::name::APM_ERROR_VALUE, model::ilp::value::APM_DATABASE_ERROR );
return O{ std::unexpect, "Error creating token" };
}
db::impl::clearSessions( user, token, apm );
oidc.jwtId = token.id;
oidc.metadata.modified = std::chrono::system_clock::now();
WRAP_CODE_LINE( const auto _os = db::update( oidc, apm ); )
if ( _os != 200 )
{
LOG_WARN << "Error updating OIDC info with jwt id " << token.id << ". APM id: " << apm.id;
cp.values.try_emplace( model::ilp::name::APM_ERROR_VALUE, "Error updating OIDC info" );
return O{ std::unexpect, "Error updating OIDC info" };
}
return O{ std::in_place, std::move( token ) };
}
WRAP_CODE_LINE( auto token = createToken( *user, *oidc, apm ); )
if ( !token.has_value() ) return error( 417, token.error(), nullptr, methods, req.header, apm );
Redirect the user back to the Wt application.
auto endpoint = boost::urls::url( holder.success );
auto encoded = boost::urls::param_pct_view( "nonce", state );
endpoint.encoded_params().append( encoded );
LOG_INFO << "Writing response for " << req.path << "; redirecting to " << endpoint.c_str() << ". APM id: " << apm.id;
auto resp = Response{ req.header };
resp.headers.clear();
resp.headers.emplace( "location", endpoint.c_str() );
resp.headers.emplace( "content-length", "0" );
resp.status = 302;
resp.jwt = std::make_shared<model::JwtToken>( token.value().token() );
resp.body.clear();
resp.compressed = false;
resp.correlationId = correlationId( req );
resp.entity = model::OIDCInformation::EntityType();
return resp;