Contents
- Architectural Overview
- Configuration Options
- Configure Server Logs
- Connect to External Resources
- Configuring Synchronization
- Scheduling Synchronization
- Managing Passwords
- Managing Authentication, Authorization & RBAC
- Authentication
- Securing & Hardening OpenIDM
- Integrating Business Processes & Workflows
- Sending Email
- Errors
- References
Architectural Overview
Modular Framework
* OSGi (Felix)
* Servlet (Jetty)
Infrastructure Modules
* Scheduler (Quartz)
* Script engine (JavaScript)
* Audit logging
* Repository
– MySQL for production use
– OrientDB for evaluation use
– Repository API is based on JSON object model and RESTful Web Services
Core Services
Object model
* Java based object model
* Java object model represents of JSON object model
* Also exposes a set of triggers and functions for scripting hookups
Managed objects
* They are identity related data managed by OpenIDM
* Configurable, JSON based data structure living the repository
* Default configuration of a manged object is that of a user
* Can be access via /openidm/managed/ context
curl --header "X-OpenIDM-Username: openidm-admin" --header "X-OpenIDM-Password: openidm-admin" --request GET "http://localhost:8080/openidm/managed/..."
System Objects
* They are pluggable representations of objects on external systems, e.g. a user entry stored in external LDAP.
* Can be access via /openidm/system/ context
curl --header "X-OpenIDM-Username: openidm-admin" --header "X-OpenIDM-Password: openidm-admin" --request GET "http://localhost:8080/openidm/system/..."
Mappings
* Mappings define policies between source and target objects and their attributes during synchronization and reconciliation
* Can also define triggers for validation, customization, filtering, and transformation of source and target objects
Synchronization and Reconciliation
Access Layer
* Provides UI and public APIs (RESTful) for accessing and managing OpenIDM repository and its functions
Configuration Options
Configuration Objects
* Exposed as JSON objects
* Can be either single instance (one per installation) or multiple instances (more than one per installation)
Single Instance Configuration Objects
* audit: specifies how audit events are logged
* authentication: controls REST access
* managed: defines managed objects and their schemas
* repo.repo-type: configures internal repository, e.g. repo.orientdb or repo.jdbc
* router: specifies filters for specific operations
* sync: defines all sync and reconciliation mappings
Multiple Instance Configuration Objects
* Naming: objectname/instancename, e.g. provisioner.openicf/xml
* JSON file views are named: objectname-instancename.json, e.g. provisioner.openicf-xml.json
Configuration Over REST
* Configuration objects are exposed under /openidm/config context
* Single instance objects are under /openidm/config/objectname context
* Multiple instance objects are under /openidm/config/objectname/instancename context
# List all configuration objects: curl --request GET \ --header "X-OpenIDM-Username: openidm-admin" \ --header "X-OpenIDM-Password: openidm-admin" http://localhost:8080/openidm/config # List single instance audit configuration object: curl \ --header "X-OpenIDM-Username: openidm-admin" \ --header "X-OpenIDM-Password: openidm-admin" http://localhost:8080/openidm/config/audit # List multiple instance configuration object: curl \ --header "X-OpenIDM-Username: openidm-admin" \ --header "X-OpenIDM-Password: openidm-admin" http://localhost:8080/openidm/config/provisioner.openicf/xml
Property Substitution in Configuration
* Define properties in conf/boot.properties. For example:
PROD.location=production DEV.location=development
* Use Property in configuration files with &{} construct. For example, repo.orientdb.json:
{ "dbUrl" : "local:./db/&{&{environment}.location}-openidm", "user" : "admin", "poolMinSize" : 5, "poolMaxSize" : 20, ... }
* Property “environment” is further defined in environment variables:
# For DEV environment: export OPENIDM_OPTS="-Xmx1024m -Denvironment=PROD" ./startup.sh # For PROD environment: export OPENIDM_OPTS="-Xmx1024m -Denvironment=DEV" ./startup.sh
* Java system properties can also be used. For example:
{ "logTo" : [ { "logType" : "csv", "location" : "&{user.home}/audit", "recordDelimiter" : ";" } ] }
* Also supports nested properties
* Does not support encrypted values at this time
Configure Server Logs
* Server logging can be configured in openidm/conf/logging.properties
* Logging levels
SEVERE WARNING INFO CONFIG FINE FINER FINEST
* Set logging level in individual script:
org.forgerock.openidm.script.javascript.JavaScript.level=level
* Override log level defined in individual script:
org.forgerock.openidm.script.javascript.JavaScript.script-name.level=level
Connect to External Resources
* Resources are
– external systems
– databases
– directory servers
– other sources of identity data
to be managed and audited by an idM
* OpenIDM connects to external resources through OpenICF which can be either standalone or embedded
* Connectors are configured through files named openidm/conf/provisioner.openicf-connectorname
* Sample connectors are located in openidm/samples/provisioners directory (copy them to conf directory to use)
Accessing Remote Connectors
* Configure remote connectors in conf/provisioner.openicf.connectorinfoprovider.json
* See sample file in conf/provisioner directory
cat provisioner.openicf.connectorinfoprovider.jsn { "connectorsLocation" : "connectors", "remoteConnectorServers" : [ { "name" : "dotnet", "host" : "127.0.0.1", "port" : 8759, "useSSL" : false, "timeout" : 0, "key" : "Passw0rd" } ] }
Configure Connectors
* Via OpenICF provisioner service
* Each connector configuration is stored in a file in the conf directory named provisioner.openicf-connectorname
Connector Configuration Examples
Configuring Synchronization
Types of Synchronization
* Synchronization happens when
– OpenIDM receives a change directly: OpenIDM pushes changes immediately to all external resources
– OpenIDM discovers a change on an external resource: through reconciliation and LiveSync
Reconciliation
* Bidirectional synchronization of objects (mainly user objects) between different data stores
* Thorough and heavy weight
* Good for compliance and reports
LiveSync
* Relies on change log on the external resource (e.g. OpenDJ and AD) to determine which object has changed
* Light weight
* Might miss some changes
Flexible Data Model
Basic Data Flow Configuration
* Elements involved:
– Three types of configuration files
– A link table that OpenIDM maintains in its repository
– Scripts to check objects and manipulate attributes
Connector Configuration Files
* Maps external resource objects to OpenIDM objects
* Lives in conf directory
* Naming convention is provisioner.resource-name.json, e.g. provisioner.openicf-xml.json
* Mapping naming conventions:
– nativeName: external attribute name
– nativeType: external attribute type
{ "name": "MyLDAP", "objectTypes": { "account": { "lastName": { "type": "string", "required": true, "nativeName": "sn", "nativeType": "string" }, "homePhone": { "type": "array", "items": { "type": "string", "nativeType": "string" }, "nativeName": "homePhone", "nativeType": "string" } } } }
Synchronization Mappings File
* Single file: conf/sync.json
* Describes a set of mappings
– Each mapping specifies attribute mapping from source to target objects (i.e. directional from source to target)
– External objects are identified as system/name/object-type
* By default, synchronize all objects matching those defined in the connector config file for the resource
* Can also do
– creating new attribute
– conditional sync
– transformation
{ "mappings": [ { "name": "systemLdapAccounts_managedUser", "source": "system/MyLDAP/account", "target": "managed/user", "properties": [ { "target": "familyName", "source": "lastName" }, { "target": "homePhone", "source": "homePhone" }, /* Creates a new attribute on the target name phoneExtension with default value of 0047 */ { "target": "phoneExtension", "default": "0047" }, /* Sync conditionally (only if source email is not null) */ { "target": "mail", "comment": "Set mail if non-empty.", "source": "email", "condition": { "type": "text/javascript", "source": "(source.email != null)" } }, /* Create a new target attribute named displayName */ { "target": "displayName", "source": ""; "transform": { "type": "text/javascript", "source": "(source.lastName +', ' + source.firstName;)" } } ] } ] }
* Filters to determine if source of target is valid to be mapped
– validSource
{ "validSource": { "type": "text/javascript", "source": "source.ldapPassword != null" } }
– validTarget
{ "validTarget": { "type": "text/javascript", "source": "target.employeeType == 'internal'" } }
Scheduling Synchronization
Managing Passwords
Enforcing Password Policies
* By applying validation rules to attributes of managed user objects
* For example, to exclude the use of password strings: ‘password’,’123456′,’12345678′, ‘qwerty’, ‘abc123’:
– In conf/managed.json file:
{ "objects" : [ { "name" : "user", "properties" : [ { "name" : "password", "encryption" : { "key" : "openidm-sym-default" } } ], "onValidate" : { "type" : "text/javascript", "file" : "script/password-validator.js" } } ] }
– Password validation file (script/password-validator.js)
const dictionary = ['password','123456','12345678', 'qwerty', 'abc123']; function isValidPassword() { var cleartextObject = openidm.decrypt(object); for (var i = 0; i < dictionary.length; i++) { if (cleartextObject.password == dictionary[i]) { throw "Password Policy Violation Exception"; }; }; }; isValidPassword();
Password Synchronization
Managing Authentication, Authorization & RBAC
OpenIDM Users
Internal Users
* anonymous: for self registration. Default password is anonymous
* openidm-admin: super user. Default password is openidm-admin
Managed Users
* Users managed by OpenIDM
Authentication
Default Attributes
* login: email
* password: password
* Default attributes are defined in conf/repo.repotype.json file, e.g. repo.orientdb.json
– credential-internaluser-query
– credential-query
"credential-query" : "SELECT * FROM ${_resource} WHERE userName = '${username}'", "credential-internaluser-query" : "SELECT * FROM internal_user WHERE _openidm_id = ${username}",
* Authentication file conf/authentication.json defines currently active query
cat authentication.json { "queryId" : "credential-query", "queryOnResource" : "managed/user", "propertyMapping" : { "userId" : "_id", "userCredential" : "password", "userRoles" : "roles" }, "defaultUserRoles" : [ ] }
Authentication
* With standard header fields:
curl --user userName:password
* With OpenIDM header fields:
curl --header "X-OpenIDM-Username: openidm-admin" --header "X-OpenIDM-Password: openidm-admin"
Roles
* openidm-reg: anonymous user
* openidm-admin: super user
* openidm-authorized: authenticated users.
– Configured by defaultUserRoles property in the conf/authentication.json file:
cat authentication.json { "queryId" : "credential-query", "queryOnResource" : "managed/user", "propertyMapping" : { "userId" : "_id", "userCredential" : "password", "userRoles" : "roles" }, "defaultUserRoles" : [ ] }
* openidm-cert: authenticated by mutual SSL authentication
Authorization
* Based on REST interface URLs
* Defined in script/router-authz.js file
– Return “Access denied” to deny access:
if (!allow()) { throw "Access denied"; }
Securing & Hardening OpenIDM
TODO
Integrating Business Processes & Workflows
* Two modes of integration
– Local integration: Activiti is embedded in OpenIDM OSGI container
– Remote integration: Standalone Activiti engine
Remote Integration
Checkout Trunk
* Checkout OpenIDM trunk from SVN URL: https://svn.forgerock.org/openidm/trunk
Compile and Package Source Codes
* Install Maven 3 if not already done
* Run Maven command:
mvn package
Install OpenIDM
* Copy openidm-zip/target/openidm-2.1.0-SNAPSHOT to target machine
* Unzip
Install Activiti
* Download and unzip activiti-5.10.zip
* Change Activiti listening port from 8080 to 9090
– Edit setup/build.xml and do a global replacement of 8080 to 9090 with vi command: :%s/8080/9090/g
* Start Activiti demo app:
ant demo.start
* Access Activiti Explorer from URL: http://localhost:9090/activiti-explorer
Deploy OpenIDM Workflow to Activiti Tomcat
* Stop Tomcat
ant tomcat.stop
* Deploy openidm-workflow-remote web app to Tomcat by copying openidm-workflow-remote/target/openidm-workflow-remote-2.1.0-SNAPSHOT.war to Tomcat webapps directory:
$ cd /opt/openidm/builtFromSrc/activiti-5.10/apps/apache-tomcat-6.0.32/webapps $ pwd /opt/openidm/builtFromSrc/activiti-5.10/apps/apache-tomcat-6.0.32/webapps $ ls activiti-explorer activiti-rest docs examples host-manager manager ROOT $ cp /mnt/hgfs/vmshare/src/openidm-workflow-remote-2.1.0-SNAPSHOT.war .
* Deploy openidm workflow activiti jar file to activiti-explorer web app’s WEB-INF/lib directory
$ cd /opt/openidm/builtFromSrc/activiti-5.10/apps/apache-tomcat-6.0.32/webapps/activiti-explorer/WEB-INF/lib $ pwd /opt/openidm/builtFromSrc/activiti-5.10/apps/apache-tomcat-6.0.32/webapps/activiti-explorer/WEB-INF/lib $ cp /mnt/hgfs/vmshare/src/openidm-workflow-activiti-2.1.0-SNAPSHOT-jar-with-dependencies.jar .
* Edit Activiti Explorer config file to be able to use OpenIDM extenstions
$ cd /opt/openidm/builtFromSrc/activiti-5.10/apps/apache-tomcat-6.0.32/webapps/activiti-explorer/WEB-INF $ pwd /opt/openidm/builtFromSrc/activiti-5.10/apps/apache-tomcat-6.0.32/webapps/activiti-explorer/WEB-INF $ ls activiti-ui-context.xml applicationContext.xml classes lib web.xml $ vi applicationContext.xml
– Replace
<bean id="processEngineConfiguration" class="org.activiti.spring.SpringProcessEngineConfiguration"> <property name="dataSource" ref="dataSource" /> <property name="transactionManager" ref="transactionManager" /> <property name="databaseSchemaUpdate" value="true" /> <property name="jobExecutorActivate" value="true" /> <property name="customFormTypes"> <list> <ref bean="userFormType"/> </list> </property> </bean>
with
<bean id="processEngineConfiguration" class="org.activiti.spring.SpringProcessEngineConfiguration"> <property name="mailServerHost" value="mail.my.com" /> <property name="mailServerPort" value="26" /> <property name="mailServerUsername" value="me@my.com" /> <property name="mailServerPassword" value="secret1" /> <property name="mailServerDefaultFrom" value="activiti@my.com" /> <property name="dataSource" ref="dataSource" /> <property name="transactionManager" ref="transactionManager" /> <property name="databaseSchemaUpdate" value="true" /> <property name="jobExecutorActivate" value="true" /> <property name="customFormTypes"> <list> <ref bean="userFormType"/> </list> </property> <property name="customSessionFactories"> <list> <bean class="org.forgerock.openidm.workflow.activiti.impl.session.OpenIDMSessionFactory"> <property name="url" value="http://localhost:8080/openidm/"/> <property name="user" value="openidm-admin"/> <property name="password" value="openidm-admin"/> </bean> </list> </property> <property name="resolverFactories"> <list> <bean class="org.forgerock.openidm.workflow.activiti.impl.OpenIDMResolverFactory"></bean> <bean class="org.activiti.engine.impl.scripting.VariableScopeResolverFactory"></bean> <bean class="org.activiti.engine.impl.scripting.BeansResolverFactory"></bean> </list> </property> <property name="expressionManager"> <bean class="org.forgerock.openidm.workflow.activiti.impl.OpenIDMExpressionManager"> </bean> </property> </bean>
* Restart Activiti Demo:
cd $ACTIVITI_HOME/setup ant demo.stop ant demo.start
* Check that Activiti Explorer is accessible.
Configure OpenIDM to use Remote Activiti
* Copy sample workflow.json file to conf directory if none exists:
cd $OPENIDM_HOME cp samples/misc/workflow.json conf/
* Edit workflow.json file to point to remote Activiti instance with correct username and password (i.e. kermit/kermit)
{ "enabled" : true, "location" : "remote", "engine" : { "url" : "http://localhost:9090/openidm-workflow-remote-2.1.0-SNAPSHOT/", "username" : "kermit", "password" : "kermit" },
* Start OpenIDM
cd $OPENIDM_HOME ./startup.sh
* Test integration:
curl \ --header "X-OpenIDM-Username: openidm-admin" \ --header "X-OpenIDM-Password: openidm-admin" \ --request GET "http://localhost:8080/openidm/workflow" {"result":[{"name":"Vacation request","processDefinitionId":"vacationRequest:1:21","key":"vacationRequest"},{"name":"Helpdesk process","processDefinitionId":"escalationExample:1:22","key":"escalationExample"},{"name":"Review sales lead","processDefinitionId":"reviewSaledLead:1:23","key":"reviewSaledLead"},{"name":"Fix system failure","processDefinitionId":"fixSystemFailure:1:24","key":"fixSystemFailure"},{"name":"Expense process","processDefinitionId":"adhoc_Expense_process:1:25","key":"adhoc_Expense_process"}]}
Install the sample workflow (example.bpmn20.xml) from the openidm-workflow-activiti project
* Login remote Activiti Explorer as kermit/kermit
* Select Manage > Deployment > Upload New
* Browse to file openidm-workflow-activiti/src/main/resources/OSGI-INF/activiti/example.bpmn20.xml
* Start osgiProcess from REST API:
curl \ --header "X-OpenIDM-Username: openidm-admin" \ --header "X-OpenIDM-Password: openidm-admin" \ --request POST "http://localhost:8080/openidm/workflow/processinstance?_action=createProcessInstance" \ --data '{ "key":"osgiProcess" }' {"_id":"1010","processInstanceId":"1010","status":"ended","businessKey":null,"processDefinitionId":"osgiProcess:1:915"}
– Also check Activiti Tomcat catalina.out for osgiProcess related outputs:
$ pwd /opt/openidm/builtFromSrc/activiti-5.10/apps/apache-tomcat-6.0.32/logs $ vi catalina.out script task using expression resolver: [result:[[name:Vacation request, processDefinitionId:vacationRequest:1:21, key:vacationRequest], [name:Helpdesk process, processDefinitionId:escalationExample:1:22, key:escalationExample], [name:Review sales lead, processDefinitionId:reviewSaledLead:1:23, key:reviewSaledLead], [name:Fix system failure, processDefinitionId:fixSystemFailure:1:24, key:fixSystemFailure], [name:Expense process, processDefinitionId:adhoc_Expense_process:1:25, key:adhoc_Expense_process], [name:Osgi process, processDefinitionId:osgiProcess:1:915, key:osgiProcess]]]
* Alternatively, start osgiProcess from Activiti Explorer:
– Check Activiti Tomcat catalina.out for osgiProcess related outputs:
Sep 18, 2012 1:07:26 PM org.restlet.engine.log.LogFilter afterHandle INFO: 2012-09-18 13:07:26 127.0.0.1 kermit 127.0.0.1 9090 GET /openidm-workflow-remote-2.1.0-SNAPSHOT/ - 200 -0 8 http://localhost:9090 Restlet-Framework/2.0.15 - script task using expression resolver: [result:[[name:Vacation request, processDefinitionId:vacationRequest:1:21, key:vacationRequest], [name:Helpdesk process, processDefinitionId:escalationExample:1:22, key:escalationExample], [name:Review sales lead, processDefinitionId:reviewSaledLead:1:23, key:reviewSaledLead], [name:Fix system failure, processDefinitionId:fixSystemFailure:1:24, key:fixSystemFailure], [name:Expense process, processDefinitionId:adhoc_Expense_process:1:25, key:adhoc_Expense_process], [name:Osgi process, processDefinitionId:osgiProcess:1:915, key:osgiProcess]]] Sep 18, 2012 1:07:26 PM org.restlet.engine.log.LogFilter afterHandle INFO: 2012-09-18 13:07:26 127.0.0.1 kermit 127.0.0.1 9090 GET /openidm-workflow-remote-2.1.0-SNAPSHOT/ - 200 -0 15 http://localhost:9090 Restlet-Framework/2.0.15 - script task using resolver: [result:[[name:Vacation request, processDefinitionId:vacationRequest:1:21, key:vacationRequest], [name:Helpdesk process, processDefinitionId:escalationExample:1:22, key:escalationExample], [name:Review sales lead, processDefinitionId:reviewSaledLead:1:23, key:reviewSaledLead], [name:Fix system failure, processDefinitionId:fixSystemFailure:1:24, key:fixSystemFailure], [name:Expense process, processDefinitionId:adhoc_Expense_process:1:25, key:adhoc_Expense_process], [name:Osgi process, processDefinitionId:osgiProcess:1:915, key:osgiProcess]]]
Local Activiti Integration
* Local Activiti integration is included in the standard OpenIDM 2.1 build without Activiti Explorer.
* To access Activiti Explorer with Local Activiti Engine, see this post.
* To use Single User Store for both OpenIDM and Activiti, see this post.
Configure Activiti Engine
* Configured in conf/workflow.json file:
– For local embedded Activiti engine:
cat workflow.json { "enabled" : true }
– For remote standalonoe Activiti instance:
{ "enabled" : true, "location" : "remote", "engine" : { "url" : "http://localhost:9090/openidm-workflow-remote-2.1.0-SNAPSHOT/", "username" : "kermit", "password" : "kermit" },
Define Activiti Workflows
* Define BPMN 2.0 work flow file (with text editor or BPMN 2.0 editor)
* Package as a .bar file
* Copy bar file to workflow directory
* Invoke workflow using a script
* Optionally schedule workflow
Invoking Activiti Workflows
* From any trigger point within OpenIDM
* From script files using openidm.action() function
/* * Calling 'myWorkflow' workflow */ var workflow = { "_action" : "myWorkflow" }; var params = { "foo" : "bar" }; openidm.action("workflow/activiti", workflow, params);
* Directly from REST interface
curl --header "X-OpenIDM-Username: openidm-admin" \ --header "X-OpenIDM-Password: openidm-admin" \ --data '{"foo":"bar"}' \ --request POST "http://localhost:8080/openidm/workflow?_action=myWorkflow"
Email Notification Example
* Create EmailNotification.bpmn file
* Package as .bar file
* Copy .bar file to workflow directory
* Invoke directly from REST
curl \ --header "X-OpenIDM-Username: openidm-admin" \ --header "X-OpenIDM-Password: openidm-admin" \ --request POST "http://localhost:8080/openidm/workflow/processinstance?_action=createProcessInstance" \ --data '{"key":"EmailNotification","fromSender" : "noreply@openidm","toEmail" : "jdoe@example.com"}' {"_id":"208","processInstanceId":"208","status":"ended","businessKey":null,"processDefinitionId":"EmailNotification:1:207"}
Example Sunset Workflow Triggered By Reconciliation
* Authoritative data source: CSV file
"firstName","uid","lastName","email","employeeNumber",password,"sunrise","sunset","active" "Darth","DDOE","Doe","doe@example.com","123456","Z29vZA==","2012-06-30T00:00:00Z","2012-12-23T00:00:00Z","TRUE"
* Target data source: XML file
* Scenario:
New user in CSV -> Kicks of reconcile process -> Found new user -> Add to XML
Sending Email
Setup Outbound Email
* Shutdown OpenIDM
* Copy sample email config file to conf directory:
cd $OPENIDM_HOME cp samples/misc/external.email.json conf/
* Modify email config file:
{ "host" : "smtp.example.com", "port" : "25", "username" : "openidm", "password" : "secret12", "mail.smtp.auth" : "true", "mail.smtp.starttls.enable" : "true" }
* Restart OpenIDM
* Check external mail is up and running
-> scr list ... [ 6] [active ] org.forgerock.openidm.external.email ...
Send mail from REST
curl \ --header "X-OpenIDM-Username: openidm-admin" \ --header "X-OpenIDM-Password: openidm-admin" \ --request POST "http://localhost:8080/openidm/external/email?_from=openidm@example.com&_to=user@example.com&_subject=Test&_body=Test" {"status":"OK"}
Sending Mail from Script
* Use external/email context:
var params = new Object(); params._from = "openidm@example.com"; params._to = "admin@example.com"; params._cc = "wally@example.com,dilbert@example.com"; params._subject = "OpenIDM recon report"; params._type = "text/html"; params._body = "<html><body><p>Recon report follows...</p></body></html>"; openidm.action("external/email", params);
Errors
action method not implemented on workflow
* Error message:
curl \ --header "X-OpenIDM-Username: openidm-admin" \ --header "X-OpenIDM-Password: openidm-admin" \ --request POST "http://localhost:8080/openidm/workflow?_action=osgiProcess" curl \ > --header "X-OpenIDM-Username: openidm-admin" \ > --header "X-OpenIDM-Password: openidm-admin" \ > --request POST "http://localhost:8080/openidm/workflow?_action=osgiProcess" {"error":400,"reason":"Bad Request","detail":"action method not implemented on workflow"}
* Reason: OpenIDM/Activiti REST interface has changed.
References
* OpenIDM 2.1.0 Integrator’s Guide
* ForgeRock Documentation
* OpenIDM Wiki
* Forum
* Enhanced REST interface for the Activiti integration
Hi,
I looked for an email address but couldn’t find one. My name is Amy Spitzfaden-Both and I’m working with PortalGuard. We’re in the field of internet security, and we’re beginning to develop relationships with respected content creators. Our focuses include self service password reset education, two factor authentication education, and single sign-on education. Would you be interested in having us post a guest blog and perhaps featuring a guest blog from you as well? You can out more about us and what we’re trying to do on our site: http://www.portalguard.com/student-portal-login.html. I look forward to hearing from you.
Amy