Decouple SGU authentication from profile pages
This commit is contained in:
@@ -12,13 +12,14 @@ Directory synchronization, deployment scripts, and tests.
|
|||||||
|
|
||||||
1. The Windows tile collects a `DO`, `AL`, or `AD` institutional key and a password.
|
1. The Windows tile collects a `DO`, `AL`, or `AD` institutional key and a password.
|
||||||
2. It sends that exact password over mutually authenticated TLS to the broker.
|
2. It sends that exact password over mutually authenticated TLS to the broker.
|
||||||
3. The broker validates the same key/password pair against the configured SGU
|
3. The broker validates the same key/password pair against the lightweight SGU
|
||||||
NTLM endpoint. The same logical authenticated request reads the minimum
|
NTLM root. Only an authoritative `401`/`403` rejects the credential.
|
||||||
available SGU profile fields.
|
4. After successful authentication, the broker makes a separately bounded,
|
||||||
4. On success, the broker creates or moves the AD user, updates the available
|
best-effort request for the minimum available SGU profile fields.
|
||||||
|
5. On success, the broker creates or moves the AD user, updates the available
|
||||||
name/mail/title/department/address metadata when available, and sets the AD password
|
name/mail/title/department/address metadata when available, and sets the AD password
|
||||||
to the exact submitted password.
|
to the exact submitted password.
|
||||||
5. The Credential Provider serializes the original `SecureString` to Windows.
|
6. The Credential Provider serializes the original `SecureString` to Windows.
|
||||||
|
|
||||||
No derived password is created. Passwords are not written to a database, file,
|
No derived password is created. Passwords are not written to a database, file,
|
||||||
event log, application log, command line, or response.
|
event log, application log, command line, or response.
|
||||||
@@ -32,7 +33,8 @@ name, email, career, and postal address. The career becomes an AD title in the
|
|||||||
form `Estudiante de ...`; faculty/department remains unset because the verified
|
form `Estudiante de ...`; faculty/department remains unset because the verified
|
||||||
page does not expose it. Professors retain the menu display-name fallback until
|
page does not expose it. Professors retain the menu display-name fallback until
|
||||||
a richer role-specific page is verified. Missing or changed presentation HTML
|
a richer role-specific page is verified. Missing or changed presentation HTML
|
||||||
never blocks authentication or password synchronization.
|
never blocks authentication or password synchronization after the lightweight
|
||||||
|
NTLM root has accepted the credential.
|
||||||
|
|
||||||
Operational documentation:
|
Operational documentation:
|
||||||
|
|
||||||
|
|||||||
+12
-8
@@ -7,8 +7,8 @@ LogonUI
|
|||||||
-> SGU Credential Provider (SecureString)
|
-> SGU Credential Provider (SecureString)
|
||||||
-> HTTPS 1.1 + client certificate
|
-> HTTPS 1.1 + client certificate
|
||||||
-> SGU Auth Broker
|
-> SGU Auth Broker
|
||||||
-> SGU IIS NTLM endpoint (original password)
|
-> SGU IIS lightweight NTLM root (original password)
|
||||||
-> minimum SGU profile metadata (same authenticated response)
|
-> minimum SGU profile metadata (bounded, best effort)
|
||||||
-> Active Directory (same original password + optional profile)
|
-> Active Directory (same original password + optional profile)
|
||||||
<- domain + canonical username; never a password
|
<- domain + canonical username; never a password
|
||||||
-> Windows credential serialization (original SecureString)
|
-> Windows credential serialization (original SecureString)
|
||||||
@@ -20,12 +20,16 @@ It follows only HTTPS redirects whose host appears in `AllowedRedirectHosts`,
|
|||||||
which prevents credential forwarding to an unexpected redirect target. HTTP/1.1
|
which prevents credential forwarding to an unexpected redirect target. HTTP/1.1
|
||||||
is forced because NTLM authentication is connection-bound.
|
is forced because NTLM authentication is connection-bound.
|
||||||
|
|
||||||
The logical GET is sent directly to the administrative incident overview for
|
The authoritative logical GET is sent to `/psulsa/`, a lightweight route that
|
||||||
`AD` identities, the student information page for `AL` identities, or the
|
returns the NTLM challenge without waiting for the slow application pages. A
|
||||||
portal menu for `DO` identities. NTLM may still require its normal
|
`401` or `403` rejects the credential; an allowed `2xx` or `3xx` proves that IIS
|
||||||
challenge/response round trips on that connection. The broker keeps any
|
accepted it. The broker then makes a separately bounded, best-effort GET to the
|
||||||
transient portal cookie in an in-memory per-request container; it is never
|
administrative incident overview for `AD`, the student information page for
|
||||||
persisted or returned to the client.
|
`AL`, or the portal menu for `DO`. A profile timeout does not invalidate an
|
||||||
|
already authenticated credential. NTLM may still require its normal
|
||||||
|
challenge/response round trips on the connection. Transient portal cookies are
|
||||||
|
kept only in an in-memory per-request container and are never persisted or
|
||||||
|
returned to the client.
|
||||||
|
|
||||||
## Offline authentication
|
## Offline authentication
|
||||||
|
|
||||||
|
|||||||
@@ -47,10 +47,12 @@ Eso es comportamiento esperado, no una caída del servicio.
|
|||||||
## Timeouts y recuperación
|
## Timeouts y recuperación
|
||||||
|
|
||||||
- El Credential Provider espera hasta **35 segundos** por el broker.
|
- El Credential Provider espera hasta **35 segundos** por el broker.
|
||||||
- El broker espera hasta **30 segundos** por SGU. Este margen cubre las
|
- El broker permite hasta **20 segundos** para el desafío NTLM ligero de
|
||||||
degradaciones observadas del portal sin bloquear LogonUI indefinidamente; el
|
`/psulsa/` y hasta **10 segundos totales** adicionales para enriquecer el
|
||||||
cliente conserva cinco segundos adicionales para que el broker cierre la
|
perfil. La consulta de perfil es best effort: si la página pesada queda
|
||||||
respuesta de manera limpia.
|
congelada después de que NTLM aceptó la contraseña, el usuario se sincroniza
|
||||||
|
sin metadatos y puede iniciar sesión. El máximo combinado queda por debajo de
|
||||||
|
los 35 segundos del cliente.
|
||||||
- El instalador configura recuperación del servicio con reinicios a los 5, 15
|
- El instalador configura recuperación del servicio con reinicios a los 5, 15
|
||||||
y 60 segundos y reinicia el contador de fallos después de 24 horas.
|
y 60 segundos y reinicia el contador de fallos después de 24 horas.
|
||||||
- Si el broker o SGU no está disponible, el Credential Provider entrega la
|
- Si el broker o SGU no está disponible, el Credential Provider entrega la
|
||||||
|
|||||||
+6
-3
@@ -24,9 +24,9 @@
|
|||||||
- Client private keys are non-exportable and reside in `LocalMachine\My`.
|
- Client private keys are non-exportable and reside in `LocalMachine\My`.
|
||||||
- The NTLM validator rejects non-HTTPS redirects, URI user information, and hosts
|
- The NTLM validator rejects non-HTTPS redirects, URI user information, and hosts
|
||||||
outside its explicit redirect allow-list.
|
outside its explicit redirect allow-list.
|
||||||
- Profile enrichment reads only allow-listed HTTPS pages and caps the response
|
- Authentication and profile enrichment read only allow-listed HTTPS pages.
|
||||||
body at 512 KiB by default. Portal cookies are request-scoped and held only in
|
Profile bodies are capped at 512 KiB by default; portal cookies are
|
||||||
memory.
|
request-scoped and held only in memory.
|
||||||
|
|
||||||
## Profile minimization
|
## Profile minimization
|
||||||
|
|
||||||
@@ -43,6 +43,9 @@
|
|||||||
before metadata is synchronized.
|
before metadata is synchronized.
|
||||||
- If SGU changes its HTML, authentication and exact-password synchronization
|
- If SGU changes its HTML, authentication and exact-password synchronization
|
||||||
continue without enrichment; existing AD metadata is not erased.
|
continue without enrichment; existing AD metadata is not erased.
|
||||||
|
- Slow profile pages cannot change an accepted credential into a rejection. The
|
||||||
|
lightweight NTLM root is authoritative; enrichment has its own shorter total
|
||||||
|
timeout.
|
||||||
|
|
||||||
Lab self-signed certificates are appropriate only for the isolated VM network.
|
Lab self-signed certificates are appropriate only for the isolated VM network.
|
||||||
Use an enterprise CA with revocation checking in production.
|
Use an enterprise CA with revocation checking in production.
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ param(
|
|||||||
[string]$NtlmEndpoint = 'https://sgu.ulsa.edu.mx/',
|
[string]$NtlmEndpoint = 'https://sgu.ulsa.edu.mx/',
|
||||||
[string[]]$AllowedNtlmRedirectHosts = @('sgu.ulsa.edu.mx'),
|
[string[]]$AllowedNtlmRedirectHosts = @('sgu.ulsa.edu.mx'),
|
||||||
[ValidatePattern('^/')]
|
[ValidatePattern('^/')]
|
||||||
|
[string]$AuthenticationPath = '/psulsa/',
|
||||||
|
[ValidatePattern('^/')]
|
||||||
[string]$AdministrativeProfilePath = '/psulsa/gadmon/capitalhumano/controlincidencias/incidencias.aspx',
|
[string]$AdministrativeProfilePath = '/psulsa/gadmon/capitalhumano/controlincidencias/incidencias.aspx',
|
||||||
[ValidatePattern('^/')]
|
[ValidatePattern('^/')]
|
||||||
[string]$StudentProfilePath = '/psulsa/alumnos/consultainformacionalumnos/consultainformacion.aspx',
|
[string]$StudentProfilePath = '/psulsa/alumnos/consultainformacionalumnos/consultainformacion.aspx',
|
||||||
@@ -28,7 +30,9 @@ param(
|
|||||||
[ValidateLength(1, 64)]
|
[ValidateLength(1, 64)]
|
||||||
[string]$DefaultCompany = 'Universidad La Salle',
|
[string]$DefaultCompany = 'Universidad La Salle',
|
||||||
[ValidateRange(10, 60)]
|
[ValidateRange(10, 60)]
|
||||||
[int]$NtlmTimeoutSeconds = 30,
|
[int]$NtlmTimeoutSeconds = 20,
|
||||||
|
[ValidateRange(2, 30)]
|
||||||
|
[int]$ProfileTimeoutSeconds = 10,
|
||||||
[switch]$CreateMissingOus,
|
[switch]$CreateMissingOus,
|
||||||
[switch]$DisableCertificateRevocationCheckForLab
|
[switch]$DisableCertificateRevocationCheckForLab
|
||||||
)
|
)
|
||||||
@@ -141,7 +145,9 @@ $productionSettings = @{
|
|||||||
Endpoint = $NtlmEndpoint
|
Endpoint = $NtlmEndpoint
|
||||||
Domain = ''
|
Domain = ''
|
||||||
TimeoutSeconds = $NtlmTimeoutSeconds
|
TimeoutSeconds = $NtlmTimeoutSeconds
|
||||||
|
ProfileTimeoutSeconds = $ProfileTimeoutSeconds
|
||||||
MaxRedirects = 5
|
MaxRedirects = 5
|
||||||
|
AuthenticationPath = $AuthenticationPath
|
||||||
AdministrativeProfilePath = $AdministrativeProfilePath
|
AdministrativeProfilePath = $AdministrativeProfilePath
|
||||||
StudentProfilePath = $StudentProfilePath
|
StudentProfilePath = $StudentProfilePath
|
||||||
MenuProfilePath = $MenuProfilePath
|
MenuProfilePath = $MenuProfilePath
|
||||||
|
|||||||
@@ -31,7 +31,9 @@ public sealed class BrokerOptions
|
|||||||
throw new InvalidOperationException("The NTLM endpoint host must be present in AllowedRedirectHosts.");
|
throw new InvalidOperationException("The NTLM endpoint host must be present in AllowedRedirectHosts.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Ntlm.TimeoutSeconds is < 2 or > 60 || Ntlm.MaxRedirects is < 0 or > 10)
|
if (Ntlm.TimeoutSeconds is < 2 or > 60 ||
|
||||||
|
Ntlm.ProfileTimeoutSeconds is < 2 or > 30 ||
|
||||||
|
Ntlm.MaxRedirects is < 0 or > 10)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("NTLM timeout or redirect limits are outside the supported range.");
|
throw new InvalidOperationException("NTLM timeout or redirect limits are outside the supported range.");
|
||||||
}
|
}
|
||||||
@@ -43,6 +45,7 @@ public sealed class BrokerOptions
|
|||||||
|
|
||||||
foreach (string profilePath in new[]
|
foreach (string profilePath in new[]
|
||||||
{
|
{
|
||||||
|
Ntlm.AuthenticationPath,
|
||||||
Ntlm.AdministrativeProfilePath,
|
Ntlm.AdministrativeProfilePath,
|
||||||
Ntlm.StudentProfilePath,
|
Ntlm.StudentProfilePath,
|
||||||
Ntlm.MenuProfilePath
|
Ntlm.MenuProfilePath
|
||||||
@@ -117,10 +120,14 @@ public sealed class NtlmOptions
|
|||||||
|
|
||||||
public string Domain { get; init; } = string.Empty;
|
public string Domain { get; init; } = string.Empty;
|
||||||
|
|
||||||
public int TimeoutSeconds { get; init; } = 30;
|
public int TimeoutSeconds { get; init; } = 20;
|
||||||
|
|
||||||
|
public int ProfileTimeoutSeconds { get; init; } = 10;
|
||||||
|
|
||||||
public int MaxRedirects { get; init; } = 5;
|
public int MaxRedirects { get; init; } = 5;
|
||||||
|
|
||||||
|
public string AuthenticationPath { get; init; } = "/psulsa/";
|
||||||
|
|
||||||
public string AdministrativeProfilePath { get; init; } =
|
public string AdministrativeProfilePath { get; init; } =
|
||||||
"/psulsa/gadmon/capitalhumano/controlincidencias/incidencias.aspx";
|
"/psulsa/gadmon/capitalhumano/controlincidencias/incidencias.aspx";
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ public sealed class NtlmCredentialValidator(BrokerOptions options) : INtlmCreden
|
|||||||
string password,
|
string password,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
Uri current = GetProfileUri(identity.Role);
|
Uri authenticationUri = new(
|
||||||
|
new Uri(options.Endpoint, UriKind.Absolute),
|
||||||
|
options.AuthenticationPath);
|
||||||
HashSet<string> allowedHosts = new(
|
HashSet<string> allowedHosts = new(
|
||||||
this.options.AllowedRedirectHosts,
|
this.options.AllowedRedirectHosts,
|
||||||
StringComparer.OrdinalIgnoreCase);
|
StringComparer.OrdinalIgnoreCase);
|
||||||
@@ -50,22 +52,57 @@ public sealed class NtlmCredentialValidator(BrokerOptions options) : INtlmCreden
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
for (int hop = 0; hop <= this.options.MaxRedirects; hop++)
|
NtlmValidationResult? authenticationFailure = await ValidateCredentialsAsync(
|
||||||
|
client,
|
||||||
|
authenticationUri,
|
||||||
|
allowedHosts,
|
||||||
|
credentialCache,
|
||||||
|
credentialedAuthorities,
|
||||||
|
credential,
|
||||||
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
if (authenticationFailure is not null)
|
||||||
{
|
{
|
||||||
if (!IsAllowedHttpsUri(current, allowedHosts))
|
return authenticationFailure;
|
||||||
{
|
|
||||||
return NtlmValidationResult.Unavailable("NTLM_REDIRECT_REJECTED");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
string authority = current.GetLeftPart(UriPartial.Authority);
|
InstitutionalProfile? profile = await TryFetchProfileAsync(
|
||||||
if (credentialedAuthorities.Add(authority))
|
client,
|
||||||
|
identity,
|
||||||
|
allowedHosts,
|
||||||
|
credentialCache,
|
||||||
|
credentialedAuthorities,
|
||||||
|
credential,
|
||||||
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
return NtlmValidationResult.Valid(profile);
|
||||||
|
}
|
||||||
|
finally
|
||||||
{
|
{
|
||||||
credentialCache.Add(new Uri(authority + "/"), "NTLM", credential);
|
credential.Password = string.Empty;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
using HttpRequestMessage request = new(HttpMethod.Get, current);
|
private async Task<NtlmValidationResult?> ValidateCredentialsAsync(
|
||||||
|
HttpClient client,
|
||||||
|
Uri authenticationUri,
|
||||||
|
HashSet<string> allowedHosts,
|
||||||
|
CredentialCache credentialCache,
|
||||||
|
HashSet<string> credentialedAuthorities,
|
||||||
|
NetworkCredential credential,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (!IsAllowedHttpsUri(authenticationUri, allowedHosts))
|
||||||
|
{
|
||||||
|
return NtlmValidationResult.Unavailable("NTLM_AUTH_ENDPOINT_REJECTED");
|
||||||
|
}
|
||||||
|
|
||||||
|
AddCredential(
|
||||||
|
authenticationUri,
|
||||||
|
credentialCache,
|
||||||
|
credentialedAuthorities,
|
||||||
|
credential);
|
||||||
|
using HttpRequestMessage request = new(HttpMethod.Get, authenticationUri);
|
||||||
using CancellationTokenSource timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
using CancellationTokenSource timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||||
timeout.CancelAfter(TimeSpan.FromSeconds(this.options.TimeoutSeconds));
|
timeout.CancelAfter(TimeSpan.FromSeconds(options.TimeoutSeconds));
|
||||||
|
|
||||||
HttpResponseMessage response;
|
HttpResponseMessage response;
|
||||||
try
|
try
|
||||||
@@ -91,17 +128,63 @@ public sealed class NtlmCredentialValidator(BrokerOptions options) : INtlmCreden
|
|||||||
}
|
}
|
||||||
|
|
||||||
int statusCode = (int)response.StatusCode;
|
int statusCode = (int)response.StatusCode;
|
||||||
if (statusCode >= 500)
|
if (statusCode is >= 200 and < 300)
|
||||||
{
|
{
|
||||||
return NtlmValidationResult.Unavailable("NTLM_UPSTREAM_ERROR");
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (statusCode is >= 300 and < 400)
|
||||||
|
{
|
||||||
|
Uri? location = response.Headers.Location;
|
||||||
|
Uri? redirect = location is null
|
||||||
|
? null
|
||||||
|
: location.IsAbsoluteUri ? location : new Uri(authenticationUri, location);
|
||||||
|
return redirect is not null && IsAllowedHttpsUri(redirect, allowedHosts)
|
||||||
|
? null
|
||||||
|
: NtlmValidationResult.Unavailable("NTLM_AUTH_REDIRECT_REJECTED");
|
||||||
|
}
|
||||||
|
|
||||||
|
return statusCode is 429 or >= 500
|
||||||
|
? NtlmValidationResult.Unavailable("NTLM_UPSTREAM_ERROR")
|
||||||
|
: NtlmValidationResult.Unavailable("NTLM_UNEXPECTED_RESPONSE");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<InstitutionalProfile?> TryFetchProfileAsync(
|
||||||
|
HttpClient client,
|
||||||
|
UserIdentity identity,
|
||||||
|
HashSet<string> allowedHosts,
|
||||||
|
CredentialCache credentialCache,
|
||||||
|
HashSet<string> credentialedAuthorities,
|
||||||
|
NetworkCredential credential,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Uri current = GetProfileUri(identity.Role);
|
||||||
|
using CancellationTokenSource timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||||
|
timeout.CancelAfter(TimeSpan.FromSeconds(options.ProfileTimeoutSeconds));
|
||||||
|
|
||||||
|
for (int hop = 0; hop <= options.MaxRedirects; hop++)
|
||||||
|
{
|
||||||
|
if (!IsAllowedHttpsUri(current, allowedHosts))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
AddCredential(current, credentialCache, credentialedAuthorities, credential);
|
||||||
|
using HttpRequestMessage request = new(HttpMethod.Get, current);
|
||||||
|
using HttpResponseMessage response = await client
|
||||||
|
.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, timeout.Token)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
int statusCode = (int)response.StatusCode;
|
||||||
if (statusCode is >= 300 and < 400)
|
if (statusCode is >= 300 and < 400)
|
||||||
{
|
{
|
||||||
Uri? location = response.Headers.Location;
|
Uri? location = response.Headers.Location;
|
||||||
if (location is null)
|
if (location is null)
|
||||||
{
|
{
|
||||||
return NtlmValidationResult.Unavailable("NTLM_INVALID_REDIRECT");
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
current = location.IsAbsoluteUri ? location : new Uri(current, location);
|
current = location.IsAbsoluteUri ? location : new Uri(current, location);
|
||||||
@@ -110,23 +193,39 @@ public sealed class NtlmCredentialValidator(BrokerOptions options) : INtlmCreden
|
|||||||
|
|
||||||
if (statusCode is >= 200 and < 300)
|
if (statusCode is >= 200 and < 300)
|
||||||
{
|
{
|
||||||
InstitutionalProfile? profile = await TryReadProfileAsync(
|
return await TryReadProfileAsync(
|
||||||
response,
|
response,
|
||||||
identity,
|
identity,
|
||||||
timeout.Token,
|
timeout.Token,
|
||||||
cancellationToken).ConfigureAwait(false);
|
cancellationToken).ConfigureAwait(false);
|
||||||
return NtlmValidationResult.Valid(profile);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return NtlmValidationResult.Invalid();
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||||
return NtlmValidationResult.Unavailable("NTLM_REDIRECT_LIMIT");
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
{
|
||||||
credential.Password = string.Empty;
|
throw;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Enrichment is optional once the lightweight NTLM endpoint has
|
||||||
|
// authoritatively accepted the credentials.
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddCredential(
|
||||||
|
Uri uri,
|
||||||
|
CredentialCache credentialCache,
|
||||||
|
HashSet<string> credentialedAuthorities,
|
||||||
|
NetworkCredential credential)
|
||||||
|
{
|
||||||
|
string authority = uri.GetLeftPart(UriPartial.Authority);
|
||||||
|
if (credentialedAuthorities.Add(authority))
|
||||||
|
{
|
||||||
|
credentialCache.Add(new Uri(authority + "/"), "NTLM", credential);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,8 +29,10 @@
|
|||||||
"Ntlm": {
|
"Ntlm": {
|
||||||
"Endpoint": "https://sgu.ulsa.edu.mx/",
|
"Endpoint": "https://sgu.ulsa.edu.mx/",
|
||||||
"Domain": "",
|
"Domain": "",
|
||||||
"TimeoutSeconds": 30,
|
"TimeoutSeconds": 20,
|
||||||
|
"ProfileTimeoutSeconds": 10,
|
||||||
"MaxRedirects": 5,
|
"MaxRedirects": 5,
|
||||||
|
"AuthenticationPath": "/psulsa/",
|
||||||
"AdministrativeProfilePath": "/psulsa/gadmon/capitalhumano/controlincidencias/incidencias.aspx",
|
"AdministrativeProfilePath": "/psulsa/gadmon/capitalhumano/controlincidencias/incidencias.aspx",
|
||||||
"StudentProfilePath": "/psulsa/alumnos/consultainformacionalumnos/consultainformacion.aspx",
|
"StudentProfilePath": "/psulsa/alumnos/consultainformacionalumnos/consultainformacion.aspx",
|
||||||
"MenuProfilePath": "/psulsa/menu.aspx",
|
"MenuProfilePath": "/psulsa/menu.aspx",
|
||||||
|
|||||||
Reference in New Issue
Block a user