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 returned model is:
var model = new { Key = "Safewhere" };
Define @model dynamic
at the top of the Razor view:
@model dynamic
<!-- Access its properties directly -->
<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, if the returned model is:
var model = new { Key = "Safewhere" };
You can access its properties directly as follows:
<!-- 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 an 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.".
You can set it up using the following steps:
1. Prepare a script content
Similar to 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 the SocialSecurityNumberConfirmationView
view to be available in the Runtime to work. To use it with the Identity Provider interceptor, the view needs to be customized as shown below:
2. Create a script in the Script library
Create a new script of the Identity Provider interceptor type with the content provided above.
Note that the script must be created in the Script library before configuring the Identity Provider that will use it.
3. Set up a scripting interceptor for an Identity Provider
Open the Identity Provider and configure it as follows:
- 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 a Service Provider using the Identity Provider configured in step 3. The SocialSecurityNumberConfirmationView
will be displayed as shown 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. The 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.
You can set it up using the following steps:
1. Prepare a script content
Similar to implementing the external protocol connection's interceptor, the interceptor script 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 thePartnerSelectionView
view to be available in the Runtime to work. To use it with the Service Provider interceptor, the view needs to be customized as shown below:
2. Create a script in Script library
Create a new script of the Service Provider interceptor type with the content provided 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 (Application) and configure it as follows:
- 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: This can be a built-in view or a custom hosted form. In this scenario, leave this field empty, as the main view is specified within the script content.
Additionally, the SourcePartnerClaimType
and DestinationPartnerClaimType
settings must be configured on the Service Provider's Interceptor tab. The ValidValueRegEx
setting is optional and can be used to filter the values of the SourcePartnerClaimType
claim.
4. Run
Access to the Service Provider and select an upstream Identity Provider to log in. After logging in successfully, the upstream Identity Provider returns a list of claims that includes the SourcePartnerClaimType claim. The PartnerSelectionView
page is then displayed as shown 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 to 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:
- Convert
@Safewhere.Web.Mvc.Security.HtmlHelper.AntiForgeryToken()
to:
{{ Csrf }}
RenderFormParameters
partial view builds some hidden parameters for the submit form based on the ContextId. In the scripting interceptor, you can use the following code snippet 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 RenderFormParameters
is the contextId
parameter of the Intercept/OnPostBack method.
After creating the parameters using RenderFormParameters
, bind this value to the binding model for the Hosted form. The hosted form will then access this parameter to render the form.
Example of a hosted form used to submit data
The following script is the hosted form version of the SocialSecurityNumberConfirmationView
Razor view:
<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>
Provision custom text resources
Assume that you have updated the "GlobalTextResource" container with the following values:
[
{
"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"
}
]
}
]
Put everything into use
Create a Service Provider interceptor script and enable it for use in the Service Provider. The script binds the ModelWithContext
model to the TestHostedFormView
view.
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; }
}
The resulting view is displayed as shown below: