OpenAM - Getting a Session attribute into an OpenID Connect claim - saml

I'm using OpenAM 13.5 configured to have a SAML circle of trust to federate logins to our applications with third-party IdPs. Some of the SAML assertions received by the third party are mapped as Session level attributes. The SAML part is working fine, but I need to connect to OpenAM an application who can talk OpenID Connect. I created an OpenID Connect service, configured the client accordingly and I can login successfully using the flow "App -> OpenAM UI -> 3rd party IDP -> OpenAM OIDC -> App".
The problem is that I can retrieve only the attributes that are mapped to the data store - the session attributes (e.g. AuthLevel, IDP Name, etc) aren't included in the mapped claims.
I tried to edit the OIDC Claims default script which has a session variable that seems to contain what I need, but unfortunately the session variable is always null.
Is this the correct approach? Why is the session null? Is there something I need to enable in order to read it?
Thanks in advance for your help.

You can not retrieve an SSO session property in OIDC claimscript because the OAuth2 client does not send the SSO tracking cookie in the token request.
It's only possible if you use AM proprietary feature 'always include claims in ID token'.

As described by Bernhard in the comments, once a request arrives to the /userinfo endpoint OpenAM has no way to reconcile the access token with a living session (and the session could not exist any more as well).
However when accessing the claims inside the ID Token by activating the proprietary AM feature "Always include claims in ID Token" the session object is available and we can poll its properties!
For future readers, this is how I modified the OIDC script:
/*
* The contents of this file are subject to the terms of the Common Development and
* Distribution License (the License). You may not use this file except in compliance with the
* License.
*
* You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
* specific language governing permission and limitations under the License.
*
* When distributing Covered Software, include this CDDL Header Notice in each file and include
* the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
* Header, with the fields enclosed by brackets [] replaced by your own identifying
* information: "Portions copyright [year] [name of copyright owner]".
*
* Copyright 2014-2016 ForgeRock AS.
*/
import com.iplanet.sso.SSOException
import com.sun.identity.idm.IdRepoException
import org.forgerock.oauth2.core.UserInfoClaims
/*
* Defined variables:
* logger - always presents, the "OAuth2Provider" debug logger instance
* claims - always present, default server provided claims
* session - present if the request contains the session cookie, the user's session object
* identity - always present, the identity of the resource owner
* scopes - always present, the requested scopes
* requestedClaims - Map<String, Set<String>>
* always present, not empty if the request contains a claims parameter and server has enabled
* claims_parameter_supported, map of requested claims to possible values, otherwise empty,
* requested claims with no requested values will have a key but no value in the map. A key with
* a single value in its Set indicates this is the only value that should be returned.
* Required to return a Map of claims to be added to the id_token claims
*
* Expected return value structure:
* UserInfoClaims {
* Map<String, Object> values; // The values of the claims for the user information
* Map<String, List<String>> compositeScopes; // Mapping of scope name to a list of claim names.
* }
*/
// user session not guaranteed to be present
boolean sessionPresent = session != null
def fromSet = { claim, attr ->
if (attr != null && attr.size() == 1){
attr.iterator().next()
} else if (attr != null && attr.size() > 1){
attr
} else if (logger.warningEnabled()) {
logger.warning("OpenAMScopeValidator.getUserInfo(): Got an empty result for claim=$claim");
}
}
attributeRetriever = { attribute, claim, identity, session, requested ->
if (requested == null || requested.isEmpty()) {
fromSet(claim, identity.getAttribute(attribute))
} else if (requested.size() == 1) {
requested.iterator().next()
} else {
throw new RuntimeException("No selection logic for $claim defined. Values: $requested")
}
}
sessionAttributeRetriever = { attribute, claim, identity, session, requested ->
if (requested == null || requested.isEmpty()) {
if (session != null) {
fromSet(claim, session.getProperty(attribute))
} else {
null
}
} else if (requested.size() == 1) {
requested.iterator().next()
} else {
throw new RuntimeException("No selection logic for $claim defined. Values: $requested")
}
}
// [ {claim}: {attribute retriever}, ... ]
claimAttributes = [
"email": attributeRetriever.curry("mail"),
"address": { claim, identity, session, requested -> [ "formatted" : attributeRetriever("postaladdress", claim, identity, session, requested) ] },
"phone_number": attributeRetriever.curry("telephonenumber"),
"given_name": attributeRetriever.curry("givenname"),
"zoneinfo": attributeRetriever.curry("preferredtimezone"),
"family_name": attributeRetriever.curry("sn"),
"locale": attributeRetriever.curry("preferredlocale"),
"name": attributeRetriever.curry("cn"),
"spid_uid": attributeRetriever.curry("employeeNumber"),
"spid_idp": attributeRetriever.curry("idpEntityId"),
"spid_gender": attributeRetriever.curry("description"),
"spid_authType": sessionAttributeRetriever.curry("AuthType"),
"spid_authLevel": sessionAttributeRetriever.curry("AuthLevel"),
]
// {scope}: [ {claim}, ... ]
scopeClaimsMap = [
"email": [ "email" ],
"address": [ "address" ],
"phone": [ "phone_number" ],
"profile": [ "given_name", "zoneinfo", "family_name", "locale", "name" ],
"spid": [ "spid_uid", "spid_idp", "spid_authType", "spid_authLevel", "spid_gender" ],
]
if (logger.messageEnabled()) {
scopes.findAll { s -> !("openid".equals(s) || scopeClaimsMap.containsKey(s)) }.each { s ->
logger.message("OpenAMScopeValidator.getUserInfo()::Message: scope not bound to claims: $s")
}
}
def computeClaim = { claim, requestedValues ->
try {
[ claim, claimAttributes.get(claim)(claim, identity, session, requestedValues) ]
} catch (IdRepoException e) {
if (logger.warningEnabled()) {
logger.warning("OpenAMScopeValidator.getUserInfo(): Unable to retrieve attribute=$attribute", e);
}
} catch (SSOException e) {
if (logger.warningEnabled()) {
logger.warning("OpenAMScopeValidator.getUserInfo(): Unable to retrieve attribute=$attribute", e);
}
}
}
def computedClaims = scopes.findAll { s -> !"openid".equals(s) && scopeClaimsMap.containsKey(s) }.inject(claims) { map, s ->
scopeClaims = scopeClaimsMap.get(s)
map << scopeClaims.findAll { c -> !requestedClaims.containsKey(c) }.collectEntries([:]) { claim -> computeClaim(claim, null) }
}.findAll { map -> map.value != null } << requestedClaims.collectEntries([:]) { claim, requestedValue ->
computeClaim(claim, requestedValue)
}
def compositeScopes = scopeClaimsMap.findAll { scope ->
scopes.contains(scope.key)
}
return new UserInfoClaims((Map)computedClaims, (Map)compositeScopes)
I also had to add the java.util.ArrayList$Itr class to the script classes whitelist.
Thanks for your help!

Related

Why does jwt verification fail? Quarkus with smallrye jwt, HS256

I have a quarkus app which does not generate jwt tokens itself but possesses a secret key of HS256-signed tokens (qwertyuiopasdfghjklzxcvbnm123456). I need to verify tokens of the incoming network requests, but for every request I get the error:
io.smallrye.jwt.auth.principal.ParseException: SRJWT07000: Failed to verify a token
...
Caused by: org.jose4j.jwt.consumer.InvalidJwtSignatureException: JWT rejected due to invalid signature. Additional details: [[9] Invalid JWS Signature: JsonWebSignature{"typ":"JWT","alg":"HS256"}->eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2NjczODI2NzIsImV4cCI6MTY5ODkxODY3MiwiYXVkIjoid3d3LmV4YW1wbGUuY29tIiwic3ViIjoianJvY2tldEBleGFtcGxlLmNvbSIsIkdpdmVuTmFtZSI6IkpvaG5ueSIsIlN1cm5hbWUiOiJSb2NrZXQiLCJFbWFpbCI6Impyb2NrZXRAZXhhbXBsZS5jb20iLCJSb2xlIjpbIk1hbmFnZXIiLCJQcm9qZWN0IEFkbWluaXN0cmF0b3IiXX0.5vBHzbTKjLnAkAIYuA3c50nWV--o9jIWV2i0GZI-aw4]
My application.properties config:
smallrye.jwt.verify.key-format=JWK
smallrye.jwt.verify.key.location=JWTSecret.jwk
smallrye.jwt.verify.algorithm=HS256
quarkus.native.resources.includes=JWTSecret.jwk
JWTSecret.jwk
{
"kty": "oct",
"k": "qwertyuiopasdfghjklzxcvbnm123456",
"alg": "HS256"
}
I tried to verify the signature of the token with jwt.io using secret key above (and it verified the signature just fine), so my guess there's something wrong with my JWK file or application.properties configuration. I also tried RS256 verification algorithm (with public/private pem keys) and it worked fine, but unfortunately I need it to work with HS256.
Below the code, but it should be ok since it works fine with other verification algorithms.
package co.ogram.domain
import org.eclipse.microprofile.jwt.JsonWebToken
import javax.annotation.security.RolesAllowed
import javax.enterprise.inject.Default
import javax.inject.Inject
import javax.ws.rs.*
import javax.ws.rs.core.Context
import javax.ws.rs.core.MediaType
import javax.ws.rs.core.SecurityContext
#Path("/secured")
class TokenSecuredResource {
#Inject
#field:Default
var jwt: JsonWebToken? = null
#GET
#Path("/roles-allowed")
#RolesAllowed("Admin")
#Produces(MediaType.TEXT_PLAIN)
fun helloRolesAllowed(#Context ctx: SecurityContext): String? {
return getResponseString(ctx!!)
}
private fun getResponseString(ctx: SecurityContext): String {
val name: String
name = if (ctx.userPrincipal == null) {
"anonymous"
} else if (ctx.userPrincipal.name != jwt!!.name) {
throw InternalServerErrorException("Principal and JsonWebToken names do not match")
} else {
ctx.userPrincipal.name
}
val type = jwt!!.getClaim<Int>("type")
return String.format(
"hello + %s,"
+ " isHttps: %s,"
+ " authScheme: %s,"
+ " type: %s,"
+ " hasJWT: %s",
name, ctx.isSecure, ctx.authenticationScheme, type, hasJwt()
)
}
private fun hasJwt(): Boolean {
return jwt!!.claimNames != null
}
}
The jose4j package does the correct verification given the JWK as an input.
Your JWT is signed with the actual octets of jwk.k ("qwertyuiopasdfghjklzxcvbnm123456").
In reality you should base64url decode the k to get a buffer to use as the HS256 secret to sign. This will align with what the jose4j package does (which is correct).

Python Eve - Dynamic Lookup Filters using authenticated user informations

I'm a newby using MongoDB and Eve; I have a problem setting up a dynamic lookup filters.
My use case is to include in a pre_GET only documents whose _id is included in a list (array) present in the profile of the (authenticated) user.
Now, when this list is static, it's working fine like this:
class BCryptAuth(BasicAuth):
def check_auth(self, username, password, allowed_roles, resource, method):
# use Eve's own db driver; no additional connections/resources are used
accounts = app.data.driver.db['people']
account = accounts.find_one({'lastname': username})
return account and \
bcrypt.hashpw(password, account['password']) == account['password']
# Hook Test
def pre_GET(resource, request, lookup):
lookup["_id"] = {'$in': ['5a19808f65a98412dba4b683', '5a1b06d365a98412a4445fa0'] }
if __name__ == '__main__':
app = Eve(auth=BCryptAuth)
# Hook Test
app.on_pre_GET += pre_GET
# End Hook Test
My need is to substitute the line
lookup["_id"] = {'$in': ['5a19808f65a98412dba4b683', '5a1b06d365a98412a4445fa0'] }
with the content of the array "canAccess" present in the authenticated user profile (a document in the collection people) - something like (pseudocode)
SELECT the content of array canAccess where lastname = authenticated_user().
This is the document representing the user:
{
"_updated": "Sat, 25 Nov 2017 14:39:11 GMT",
"firstname": "barack",
"lastname": "obama",
"role": [
"copy",
"author"
],
"canAccess": [
"5a1b06d365a98412a4445fa0",
"5a1c5c9265a984120caf7e0b"
],
"_created": "Sat, 25 Nov 2017 14:39:11 GMT",
"_id": "5a19808f65a98412dba4b683",
"_etag": "758056ac49d156526858bd3a8b4922d65231942f"
}
Any help would be greatly appreciated.
Thanks
Giulio
You could use flask g to store the user_id in the per-request application context, so you can retrieve it inside the hook.
In check_auth:
from flask import g
def check_auth(self, username, password, allowed_roles, resource, method):
people = app.data.driver.db['people']
user = people.find_one({'lastname': username})
g.user_id = user['_id']
return account and \
bcrypt.hashpw(password, account['password']) == account['password']
Then you can access your data from the array inside the hook by doing the same done in the check_auth to retrieve the account, roughly like this:
from flask import g
def pre_GET(resource, request, lookup):
user_id = getattr(g, 'user_id', None)
people = app.data.driver.db['people']
user = accounts.find_one({'_id': user_id})
lookup["_id"] = {'$in': user['canAcess']}

Is it possible to secure a ColdFusion 11 REST Service with HTTP BASIC Authentication?

I am setting up a simple REST Service in ColdFusion 11. The web server is IIS 8.5 on Windows Server 2012R2.
This REST Service needs to be secured to prevent unauthorized users from accessing/writing data. For the time being, there will be only one authorized user, so I want to keep authentication/authorization as simple as possible. My initial thought is to use HTTP BASIC Authentication.
Here's the setup for the REST Service:
Source Directory: C:\web\site1\remoteapi\
REST path: inventory
To implement this, I configured the source directory of the REST Service in IIS to authorize only one user, disable Anonymous authentication, and enable Basic authentication.
When I call the source directory directly in a browser (i.e. http://site1/remoteapi/inventory.cfc?method=read), I am presented with the Basic authentication dialog.
However, when I attempt to request the REST path (http://site1/rest/inventory/), I am not challenged at all.
How can I implement HTTP BASIC authentication on the REST path?
So, due to the need to get this done without much delay, I went ahead and using some principles from Ben Nadel's website, I wrote my own authentication into the onRequestStart() method of the REST Service's Application.cfc. Here is the basic code, though it uses hard-coded values in the VARIABLES scope to validate the username and password and also does not include any actual "authorization" setting:
public boolean function onRequestStart(required string targetPage) {
LOCAL.Response = SUPER.onRequestStart(ARGUMENTS.targetpage);
if (!StructKeyExists(GetHTTPRequestData().Headers, "Authorization")) {
cfheader(
name="WWW-Authenticate",
value="Basic realm=""REST API Access"""
);
LOCAL.RESTResponse = {
status = 401,
content = {Message = "Unauthorized"}
};
restSetResponse(LOCAL.RESTResponse);
}
else {
LOCAL.IsAuthenticated = true;
LOCAL.EncodedCredentials =
GetToken( GetHTTPRequestData().Headers.Authorization, 2, " " );
// Credential string is not Base64
if ( !ArrayLen(
REMatch(
"^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)$",
LOCAL.EncodedCredentials
)
)
) {
LOCAL.IsAuthenticated = false;
}
else {
// Convert Base64 to String
LOCAL.Credentials =
ToString(ToBinary( LOCAL.EncodedCredentials ));
LOCAL.Username = GetToken( LOCAL.Credentials, 1, ":" );
LOCAL.Password = GetToken( LOCAL.Credentials, 2, ":" );
if ( LOCAL.Username != VARIABLES.CREDENTIALS.Username
|| LOCAL.Password != VARIABLES.CREDENTIALS.Password
) {
LOCAL.IsAuthenticated = false;
}
}
if (!LOCAL.IsAuthenticated) {
LOCAL.Response = {
status = 403,
content = {Message = "Forbidden"}
};
restSetResponse(LOCAL.Response);
}
}
return LOCAL.Response;
}

AD FS email claim not found

I have a web app. I'm trying to get it to authenticate against a Win2012 R2 ADFS server.
I have the relying party set up, get redirected, sign in, then redirected back to the app as a failed request.
In the event log I have this:
MSIS7070: The SAML request contained a NameIDPolicy that was not satisfied by the issued token. Requested NameIDPolicy: AllowCreate: True Format: urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress SPNameQualifier: . Actual NameID properties: null.
If I read this right, the webapp is asking for urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress but that policy isn't found for the relying party.
Under the relying party, I have two rules:
# get email address from active directory
c:[Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname", Issuer == "AD AUTHORITY"]
=> issue(store = "Active Directory",
types = ("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"), query = ";mail;{0}", param = c.Value);
rule 2
transform email address to nameid/email
c:[Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"]
=> issue(Type = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier",
Issuer = c.Issuer,
OriginalIssuer = c.OriginalIssuer,
Value = c.Value,
ValueType = c.ValueType,
Properties["http://schemas.xmlsoap.org/ws/2005/05/identity/claimproperties/format"]
= "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress");
I've double checked and made sure that the formats match, but I'm stuck on the error messages.

AuthTicketsHelper.getTicket() returns null

My company uses Perforce for version control and I'm writing software that automates use of Perforce using p4java. I'm running into a problem where my code can't connect to the Perforce server even though I am passing in valid information to use the p4tickets file on my computer.
First, I logged on to perforce to get a p4ticket by running "p4 login", which created the ~/.p4tickets file. But when I run my program that uses p4java to connect using the p4ticket file, it returns null.
AuthTicket auth = AuthTicketsHelper.getTicket(username, serverAddr, p4TicketsFilePath);
// auth == null
I've double checked that the username I'm passing in matches the $P4USER environment variable I had when I used "p4 login", as well as that serverAddr matched the host name that was referenced by my $P4PORT. The p4TicketsFilePath also exists and is the correct path to the .p4tickets file which has my ticket, which is not expired. I'm looking for the reason why getTicket still returns null.
You can debug this issue by copying the source code from AuthTicketsHelper and insert print statements. here's my logger:
private static final Logger logger = LoggerFactory.getLogger(YourClass.class);
(if you don't have a logger, you can do System.out.println() with String.format("... %s ... %s") instead.) Then, copy in this code.
AuthTicket auth;
{
AuthTicket foundTicket = null;
String serverAddress = serverAddr;
String ticketsFilePath = p4TicketsFilePath;
String userName = username;
if (serverAddress != null) {
logger.info("P4TICKETS 1");
if (serverAddress.indexOf(':') == -1) {
serverAddress += "localhost:" + serverAddress;
logger.info("P4TICKETS 2");
}
for (AuthTicket ticket : AuthTicketsHelper.getTickets(ticketsFilePath)) {
logger.info("P4TICKETS 3 {} - {} - {} - {}", serverAddress, ticket.getServerAddress(), userName, ticket.getUserName());
if (serverAddress.equals(ticket.getServerAddress())
&& (userName == null || userName.equals(ticket
.getUserName()))) {
logger.info("P4TICKETS 4");
foundTicket = ticket;
break;
}
}
logger.info("P4TICKETS 5");
}
auth = foundTicket;
}
Follow the code path in the output to see what went wrong. In my case, the server name was a hostname in the code, but my .p4tickets file had an IP address for the server name.