Scripting Interceptors
Introduction
The Scripting Interceptor feature allows intercepting login flows using scripts instead of external DLLs. The introduced script types are Identity Provider interceptor and Service Provider interceptor.
This document provides guidance on how to use these script types to intercept a login flow and how they interacts with text resources and views (either Razor views or hosted forms).
Text resource support
The text resources used for scripting interceptors must be added to GlobalTextResources.
Customize the text resources by following the instructions found here.
Syntax:
- In the script:
Resources.GlobalTextResources.Text["TestResource"]
- In the Hosted form:
{{ Resources.GlobalTextResources.TestResource }}
- In the Razor view:
@Resources.GlobalTextResources.Text["TestResource"]
View support
The scripting interceptor can work with Razor view or Hosted form
Razor Views
To support flexible binding of any model returned by a scripting interceptor in a Razor view, configure the view to use a dynamic model. This allows you to handle different types of models without needing to know their exact structure beforehand.
Example:
if the model is returned as:
var model = new { Key = "Safewhere" };
Define @model dynamic
at the top of the Razor view:
@model dynamic
<!-- Access properties like this -->
<p>@Model.Key</p>
In this setup, you can use Model.<PropertyName>
syntax to access properties dynamically, making it adaptable to different model structures.
Hosted Forms
Customize the hosted form by following the instructions found here.
In hosted forms, you do not need to define a model at the top of the view. You can directly access the model properties using Model.<PropertyName>
, similar to Razor views.
For instance, with a returned model like:
var model = new { Key = "Safewhere" };
Access the property directly:
<!-- Access properties like this -->
<p>{{ Model.Key }}</p>
Identity Provider interceptor
Script usage
This type of script is invoked after Safewhere Identify receives a token, but before it creates a login session for a user. This enables use cases such as custom validation rules for claims that an Identity Provider must return, or asking a user for more user information before creating a login session.
You can use a script of this type to create an Interceptor to use for a Identity Provider.
Example
This example demonstrates how to set up a scripting interceptor for an Identity Provider using a Razor view.
Suppose the script's goal is to intercept the login flow with the following actions:
- Display a UI which asks for a security code. All allowed security codes can be hardcoded.
- After a user enters a code and submits, check if the code is valid. If yes, proceed to the next login step, otherwise, display another view which tells: "The code you entered is invalid. Please enter a valid code below.".
Set it up using the following steps:
1. Prepare a script content
Just like the way of implementing the external authentication connection's interceptor, the identity provider interceptor script is a C# class that implements the IAuthenticationInterceptorService interface.
The script for this interceptor will look like this:
using Safewhere.External.Interceptors;
using Safewhere.External.Model;
using Safewhere.External.Services;
public class SocialSecurityNumberConfirmationInterceptorService : IAuthenticationInterceptorService
{
/// <summary>
/// Hardcode of valid social security number for demo
/// </summary>
private List<string> ValidNumbers = new List<string>
{
"1122", "1234", "6678", "0601", "2010"
};
public ActionResult Intercept(System.Web.Mvc.ControllerContext cc, System.Security.Claims.ClaimsPrincipal principal, IIdentifyRequestInformation requestInformation, IDictionary<string, string> input, string contextId, string viewName)
{
if (cc == null)
{
throw new ArgumentNullException("cc");
}
if (principal == null)
{
throw new ArgumentNullException("principal");
}
if (input == null)
{
throw new ArgumentNullException("input");
}
if (string.IsNullOrEmpty(viewName))
{
viewName = "SocialSecurityNumberConfirmationView";
}
var sessionLoginContext = requestInformation.IdentifyLoginContext;
var endpointContext = requestInformation.GetEndpointContext();
string entityId = sessionLoginContext.GetProtocolConnectionEntityId();
Guid protocolConnectionId = sessionLoginContext.GetProtocolConnectionId();
var authenticationConnectionEntityId = requestInformation.GetAuthenticationConnectionEntityId();
Guid authenticationConnectionId = requestInformation.GetAuthenticationConnectionId();
Guid authenticationConnectionId2 = sessionLoginContext.GetAuthenticationConnectionId();
var viewResult = new ViewResult
{
ViewName = viewName,
ViewData =
{
Model = new ModelWithContextId
{
ContextId = contextId
}
}
};
return viewResult;
}
public ActionResult OnPostBack(System.Web.Mvc.ControllerContext cc, System.Security.Claims.ClaimsPrincipal principal, IIdentifyRequestInformation requestInformation, IDictionary<string, string> input, string contextId, string viewName)
{
if (cc == null)
{
throw new ArgumentNullException("cc");
}
if (principal == null)
{
throw new ArgumentNullException("principal");
}
if (input == null)
{
throw new ArgumentNullException("input");
}
//Get social number from input tag in the view (<input type='text' name='socialnumber'/>)
var socialnumber = string.Empty;
var valueProviderResult = cc.Controller.ValueProvider.GetValue("socialnumber");
if (valueProviderResult != null
&& !string.IsNullOrEmpty(valueProviderResult.AttemptedValue))
socialnumber = valueProviderResult.AttemptedValue;
//Verify the number
if (!ValidNumbers.Any(n => n == socialnumber.Trim()))
{
return Intercept(cc, principal, requestInformation, input, contextId, viewName);
}
return null;
}
public IEnumerable<string> MustHaveInputKeys
{
get { return new List<string>(); }
}
}
public class ModelWithContextId
{
public string ContextId { get; set; }
}
Note that this example requires to have the SocialSecurityNumberConfirmationView
view in the Runtime to work. To run with the Indentity Provider interceptor, the view needs to customize as shown below:
2. Create a script in Script library
Create a new script of the Identity Provider interceptor type with the content above.
Note that the script must be created in the Script library before configuring the Identity Provider that uses it.
3. Set up a scripting interceptor for an Identity Provider
Open the Identity Provider and configure as below:
- Intercept login flow: Set to Yes.
- Interceptor type name: Choose Scripting interceptor.
- Script name: Specify the desired script.
- Name of the main view which the interceptor should use: The view can be a built-in view or a custom hosted form. In this scenario, this field can be left empty, because the main view is specified in the script content.
4. Run
Log in to Safewhere Identify, select the Identity Provider at step 3. and log in successfully to the Identity Provider. The page of SocialSecurityNumberConfirmationView
is displayed as below:
Service Provider interceptor
Script usage
This type of script is invoked before claims transformation pipeline is run. The advantage of an intercepter compared to a custom claims transformation is that it allows for use cases requiring user interactions.
You can use a script of this type to create an Interceptor to use for a Service Provider.
Example
This example demonstrates how to set up a scripting interceptor for a Service Provider using a Razor view.
Suppose the script's goal is to intercept the login flow with the following actions:
- Display a UI that asks for a partner selection. A list of partners is obtained from the claim values of the claim specified in SourcePartnerClaimType.
- After a user selects a partner and submits it, assign the selected partner to the claim specified in DestinationPartnerClaimType. Then, return a list of claims to the Service Provider.
Set it up using the following steps:
1. Prepare a script content
Just like the way of implementing the external protocol connection's interceptor, the scripting one is a C# class that implements the IProtocolInterceptorService interface.
The script for this interceptor will look like this:
using Safewhere.External.Interceptors;
using Safewhere.External.Model;
using System.Text.RegularExpressions;
public class PartnerSelectionInterceptorService : IProtocolInterceptorService
{
private const string SourcePartnerClaimType = "SourcePartnerClaimType";
private const string DestinationPartnerClaimType = "DestinationPartnerClaimType";
private const string ValidValueRegEx = "ValidValueRegEx";
public ActionResult Intercept(ControllerContext cc, ClaimsPrincipal principal, IIdentifyRequestInformation requestInformation, IDictionary<string, string> input, string contextId,
string viewName)
{
if (cc == null)
{
throw new ArgumentNullException("cc");
}
if (principal == null)
{
throw new ArgumentNullException("principal");
}
if (input == null)
{
throw new ArgumentNullException("input");
}
if (string.IsNullOrEmpty(viewName))
{
viewName = "PartnerSelectionView";
}
var model = new PartnerSelectionModel
{
ContextId = contextId
};
var partners = GetPartners(principal, input);
if (partners.Count <= 1)
return null;
model.Partners = partners;
var viewResult = new ViewResult
{
ViewName = viewName,
ViewData =
{
Model = model
}
};
return viewResult;
}
private static List<string> GetPartners(ClaimsPrincipal principal, IDictionary<string, string> input)
{
string valueFilterRegEx = string.Empty;
if (input.ContainsKey(ValidValueRegEx))
valueFilterRegEx = input[ValidValueRegEx];
string sourceClaimType = input[SourcePartnerClaimType];
var partners = principal.Claims.Where(claim => claim.Type == sourceClaimType)
.Select(claim => claim.Value);
if (!string.IsNullOrEmpty(valueFilterRegEx))
{
var regEx = new Regex(valueFilterRegEx);
partners = partners.Where(claim => regEx.IsMatch(claim));
}
return partners.Distinct()
.ToList();
}
public ActionResult OnPostBack(ControllerContext cc, ClaimsPrincipal principal, IIdentifyRequestInformation requestInformation, IDictionary<string, string> input, string contextId,
string viewName)
{
if (cc == null)
{
throw new ArgumentNullException("cc");
}
if (principal == null)
{
throw new ArgumentNullException("principal");
}
if (input == null)
{
throw new ArgumentNullException("input");
}
var partner = string.Empty;
var valueProviderResult = cc.Controller.ValueProvider.GetValue("partners");
if (valueProviderResult != null
&& !string.IsNullOrEmpty(valueProviderResult.AttemptedValue))
partner = valueProviderResult.AttemptedValue;
var partners = GetPartners(principal, input);
string destinationClaimType = input[DestinationPartnerClaimType];
//Verify the number
if (partners.All(n => n != partner.Trim()))
{
return Intercept(cc, principal, requestInformation, input, contextId, viewName);
}
ClaimsIdentity identity = ((ClaimsIdentity)principal.Identity);
identity.AddClaim(new Claim(destinationClaimType, partner));
return null;
}
public IEnumerable<string> MustHaveInputKeys
{
get
{
return new List<string>
{
"SourcePartnerClaimType",
"DestinationPartnerClaimType",
"ValidValueRegEx"
};
}
}
}
public class PartnerSelectionModel
{
public string ContextId { get; set; }
public List<string> Partners { get; set; }
}
Note that this example requires to have the PartnerSelectionView
view in the Runtime to work. To run with the Service Provider interceptor, the view needs to customize some fields as shown below:
2. Create a script in Script library
Create a new script of the Service Provider interceptor type with the content above.
Note that the script must be created in the Script library before configuring the Service Provider that uses it.
3. Set up a scripting interceptor for a Service Provider
Open the Service Provider and configure as below:
- Intercept login flow: Set to Yes.
- Interceptor type name: Choose Scripting interceptor.
- Script name: Specify the desired script.
- Name of the main view which the interceptor should use: The view can be a built-in view or a custom hosted form. In this scenario, this field can be left empty, because the main view is specified in the script content.
Beside that, the SourcePartnerClaimType
and DestinationPartnerClaimType
settings must be set on the service provider's Interceptor tab. The ValidValueRegEx
is optional to filter the SourcePartnerClaimType
claim values.
4. Run
Access to the Service Provider, select an upstream identity provider to log in. After login successfully, the upstream idp returns a list of claims that contains SourcePartnerClaimType claim, the The page of PartnerSelectionView
is displayed as below:
How to set up a scripting interceptor using a hosted form and custom text resources
The following example shows how to configure a scripting interceptor for a Service Provider that displays a submit form for end users. It also includes guidance on converting a Razor view to a custom hosted form and using new text resources in that hosted form.
Conversion for Hosted form
Assume there is a Razor view for the submit form in Safewhere Identify, as shown below:
@using Safewhere.IdentityProvider.RuntimeModel
@using Safewhere.IdentityProviderModel
@using System.Web.Mvc.Html
@using Safewhere.External.Samples
@model dynamic
@section MainContent {
<h2>You are accessing a sample application.</h2>
<form method="post">
@Safewhere.Web.Mvc.Security.HtmlHelper.AntiForgeryToken()
<input type="submit" value="OK" tabindex="0" />
@Html.Partial("RenderFormParameters", new RenderFormParametersModel(Model.ContextId))
</form>
}
The Razor view uses @Safewhere.Web.Mvc.Security.HtmlHelper.AntiForgeryToken()
and RenderFormParameters
partial view. However, for the hosted form, this syntax cannot be used. We provide another solution to customize the hosted form for use in a Scripting Interceptor, as follows:
@Safewhere.Web.Mvc.Security.HtmlHelper.AntiForgeryToken()
in the Hosted form is converted to:
{{ Csrf }}
RenderFormParameters
partial view builds some hidden parameters for the submit form based on the ContextId. In the scripting interceptor, we provide the method to build these parameters:
public string RenderFormParameters(string contextId)
{
var builder = StringBuilderPool.Allocate();
var formParameters = string.Empty;
try
{
if (string.IsNullOrEmpty(HttpContext.Current.Request.QueryString[ProtocolConnectionContextItems.ContextIdKey]) && !string.IsNullOrEmpty(contextId))
{
builder.AppendLine(string.Format("<input type=\"hidden\" id=\"{0}\" name=\"{0}\" value =\"{1}\">", HttpUtility.HtmlAttributeEncode(ProtocolConnectionContextItems.ContextIdKey), HttpUtility.HtmlAttributeEncode(contextId)));
}
foreach (string name in HttpContext.Current.Request.Form.FilterSensitiveParameters())
{
if (name == ProtocolConnectionContextItems.ContextIdKey)
{
// skip because of the contextid key render above
continue;
}
builder.AppendLine(string.Format("<input type=\"hidden\" name=\"{0}\" value=\"{1}\"/>", HttpUtility.HtmlAttributeEncode(name), HttpUtility.HtmlAttributeEncode(HttpContext.Current.Request.Params[name])));
}
formParameters = builder.ToString();
}
finally
{
StringBuilderPool.Free(builder);
}
return formParameters;
}
Note: The input parameter of the RenderFormParameters
is the contextId
parameter of the Intercept/OnPostBack method.
After creating the parameters by RenderFormParameters
, bind this value to the binding model for the Hosted form, and the Hosted form accesses this parameter to render the form.
Example of a hosted form used to submit data
Assume that you have created the Hosted form named "TestHostedFormView" with the following value:
<div class="form-box">
<div class="form-top">
<div class="form-top-left text-left">
<h4> Hosted form </h4>
</div>
</div>
<div class="form-bottom">
{{ Resources.GlobalTextResources.TextResource_Example1 }}
<p>Message: {{ Model.Message }}</p>
<form method="post">
{{ Csrf }}
<input class="form-control" type="text" name="socialnumber" />
<br />
<div class="form-group">
<button type="submit" class="btn" tabindex="3">OK</button>
</div>
{{ Model.FormParameters }}
</form>
</div>
</div>
Example of custom text resources used
Assume that you have updated the "GlobalTextResource" with the following value:
[
{
"languageCode": "en",
"items": [
{
"key": "TextResource_Example1",
"value": "Text Resource Example 1"
},
{
"key": "TextResource_Example2",
"value": "Text Resource Example 2"
},
{
"key": "TextResource_Example3",
"value": "Text Resource Example 3"
}
]
},
{
"languageCode": "da",
"items": [
{
"key": "TextResource_Example4",
"value": "Text Resource Example 4"
},
{
"key": "TextResource_Example5",
"value": "Text Resource Example 5"
}
]
}
]
Example of an interceptor script for a Service Provider using custom text resources and a hosted form
Create a Service Provider interceptor script and enable it for use in the service provider. The script binds the model ModelWithContext
to the view TestHostedFormView
.
using Safewhere.External.Interceptors;
using Safewhere.External.Model;
public class TestInterceptorService : IProtocolInterceptorService
{
public ActionResult Intercept(ControllerContext cc, ClaimsPrincipal principal, IIdentifyRequestInformation requestInformation, IDictionary<string, string> input, string contextId,
string viewName)
{
if (cc == null)
{
throw new ArgumentNullException("cc");
}
if (principal == null)
{
throw new ArgumentNullException("principal");
}
if (input == null)
{
throw new ArgumentNullException("input");
}
if (string.IsNullOrEmpty(viewName))
{
viewName = "TestHostedFormView";
}
var formParameters = RenderFormParameters(contextId);
var model = new ModelWithContext
{
FormParameters = formParameters,
Message = Resources.GlobalTextResources.Text["TextResource_Example2"]
};
var viewResult = new ViewResult
{
ViewName = viewName,
ViewData =
{
Model = model
}
};
return viewResult;
}
public ActionResult OnPostBack(ControllerContext cc, ClaimsPrincipal principal, IIdentifyRequestInformation requestInformation, IDictionary<string, string> input, string contextId,
string viewName)
{
return null;
}
public IEnumerable<string> MustHaveInputKeys
{
get { return new List<string> {}; }
}
public string RenderFormParameters(string contextId)
{
var builder = StringBuilderPool.Allocate();
var formParameters = string.Empty;
try
{
if (string.IsNullOrEmpty(HttpContext.Current.Request.QueryString[ProtocolConnectionContextItems.ContextIdKey]) && !string.IsNullOrEmpty(contextId))
{
builder.AppendLine(string.Format("<input type=\"hidden\" id=\"{0}\" name=\"{0}\" value =\"{1}\">", HttpUtility.HtmlAttributeEncode(ProtocolConnectionContextItems.ContextIdKey), HttpUtility.HtmlAttributeEncode(contextId)));
}
foreach (string name in HttpContext.Current.Request.Form.FilterSensitiveParameters())
{
if (name == ProtocolConnectionContextItems.ContextIdKey)
{
// skip because of the contextid key render above
continue;
}
builder.AppendLine(string.Format("<input type=\"hidden\" name=\"{0}\" value=\"{1}\"/>", HttpUtility.HtmlAttributeEncode(name), HttpUtility.HtmlAttributeEncode(HttpContext.Current.Request.Params[name])));
}
formParameters = builder.ToString();
}
finally
{
StringBuilderPool.Free(builder);
}
return formParameters;
}
}
public class ModelWithContext
{
public string FormParameters { get; set; }
public string Message { get; set; }
}
An the result view is displayed as shown below: