Websphere 8.5.5.16, OIDC IDToken - user problem - oauth

I'm trying to configure Oauth authorization on websphere 8.5.5.16. I added interceptor with issuerIdentifier parameter = https://company.com/abc I next step I added trust external realm: https://company.com/abc And when I try to start service in my app (IBM BPM) I getting an error: NullPointer Exception. Please look at the logs on how the user is created:
Principal: https://company.com/abc/login_user
Public Credential: com.ibm.ws.security.auth.WSCredentialImpl#ebc4e0d2
Private Credential: {setLtpaCookie=false, com.ibm.wsspi.security.cred.securityName=login_user, com.ibm.wsspi.security.cred.uniqueId=user:https://company.com/abc/login_user, token_type=, access_token=xxx, id_token=, com.ibm.wsspi.security.cred.realm=https://company.com/abc, com.ibm.wsspi.security.cred.groups=[], refresh_token=, JsonWebToken=JsonWebToken:{"aud":"0000","iss":"https://company.com/abc","iat":122,"nbf":123,"exp":232,"auth_time":222,"nonce":"aaa","sub":"ddddd/fffff","upn":"login_user","unique_name":"domain\\login_user","pwd_url":"https://company.com/abc/portal/updatepassword/","pwd_exp":"4545","sid":"S-1-5-21-66-117609710","authorities":["Group_1, Group_2"],"given_name":"Name","family_name":"Surname","apptype":"Public","appid":"0000","authmethod":"http://schemas.microsoft.com/ws/2008/06/identity/authenticationmethod/windows","ver":"1.0","scp":"openid"}}
Private Credential: com.ibm.ws.security.token.SingleSignonTokenImpl#347c9d2b
Private Credential: com.ibm.ws.security.token.AuthenticationTokenImpl#718ea698
Private Credential: com.ibm.ws.security.token.AuthorizationTokenImpl#27e8a5bb
00000187 UserOrgModule 1 com.lombardisoftware.userorg.UserOrgModule getIdFromPrincipalName getIdFromPrincipalName() user=/company.com/abc/login_user, id=null
user = /company.com/abc/login_user not: login_user Why? Please help.

From message "00000187 UserOrgModule", I can tell your BPM application can not help realm name that contains '/' character. You can resolve the problem with following steps:
In your TAI properties, add useRealm property, and give an unique and meaningful value as realm, for example,
provider_.useRealm=abc123
Add "abc123" as trusted realm. This value matches value you define in step 1.
If you assign roles to users unique id, you need reassign roles again with unique id build from this new realm.

Thanks. I did exactly as you wrote.
I added in my interceptor config: provider_1.useRealm=myrealm
I added trusted realm in Global security > Federated repositories > Trusted authentication realms - inbound (Name = myrealm, Trusted).
I restarted server.
Nothing has changed. I still see: user=/company.com/abc/login_user, id=null because in JWT token, in iss field I have value: 'https://company.com/abc' and unfortunately I cannot change this

Related

How does an OAuth2 server obtain launch context for smart on FHIR?

I am integrating a Smart on FHIR app that will be launched from within an EHR. When the user clicks a button to launch the app, we set a GUID and the current Patient ID to a database record on our FHIR server. The assumption being that given the 'Launch' scope, the OAuth server will call the appropriate API to retrieve the Patient ID given that the GUID is included in the url params.
The call to auth looks like this:
_clientID = {the unique client ID registered to our auth server}
_redirectURL = {redirect back to auth for eventual token request}
launch={the GUID value generated at start of the session and paired with the Patient ID}
_scopes = "launch patient/*.* openid profile"
state = {some opaque value}
aud = {the base URL for our FHIR server}
string url = $"{authorizeURL}?response_type=code&client_id={_clientID}&" +
$"redirect_uri={_redirectURL}&" +
$"launch={launch}&" +
$"scope={HttpUtility.UrlEncode(_scopes)}&" +
$"state={state}&" +
$"aud=https://xxx-smart.xxxxxxxxx.com";
All of this works and I end up with a json response that includes the id_token, access_token, expires_in, token_type('Bearer'). But, no 'patient'.
My assumption was that the OAuth server would call the scope 'launch/patient' on our FHIR server but no such call is being made. In fact, I created a few endpoints just for the purpose of logging and NONE of them are being called.
Here is an example of one of my FHIR Server test/log endpoints (I created few with 1 to 4 parameters):
[AllowAnonymous]
[HttpGet("{functionName}/{id}")]
public string GetPatientData3(string functionName, string id)
{
TelemetryClient telemetry = new();
telemetry.TrackEvent($"FHIR SVR GetPatientData3 {functionName} {id}");
string configJson = "0009998888";
return configJson;
}
How do I set this 'patient' context properly?
How does the OAuth server retrieve this context so I can have that patient ID appear in the json response from the ~/token call?
Further Notes:
The contents of the openid-configuration:
{"token_endpoint":
"https://aadproxy.azurewebsites.net/xxx/oauth2/v2.0/token",
"token_endpoint_auth_methods_supported":
["client_secret_post","private_key_jwt","client_secret_basic"],
"jwks_uri":
"https://login.microsoftonline.com/xxx/discovery/v2.0/keys",
"response_modes_supported": ["query","fragment","form_post"],
"subject_types_supported": ["pairwise"],
"id_token_signing_alg_values_supported": ["RS256"],
"response_types_supported":["code","id_token","code
id_token","id_token token"],
"scopes_supported":["openid","profile","email","offline_access"],
"issuer": "https://login.microsoftonline.com/xxx/v2.0",
"request_uri_parameter_supported":false,
"userinfo_endpoint":"https://graph.microsoft.com/oidc/userinfo",
"authorization_endpoint":
"https://aadproxy.azurewebsites.net/xxx/oauth2/v2.0/authorize",
"device_authorization_endpoint":
"https://login.microsoftonline.com/xxx/oauth2/v2.0/devicecode",
"http_logout_supported":true,
"frontchannel_logout_supported":true,
"end_session_endpoint":
"https://login.microsoftonline.com/xxx/oauth2/v2.0/logout",
"claims_supported":
["sub","iss","cloud_instance_name","cloud_instance_host_name",
"cloud_graph_host_name","msgraph_host","aud","exp","iat",
"auth_time","acr","nonce","preferred_username",
"name","tid","ver","at_hash","c_hash","email"],
"kerberos_endpoint":
"https://login.microsoftonline.com/xxx/kerberos",
"tenant_region_scope":"NA",
"cloud_instance_name":"microsoftonline.com",
"cloud_graph_host_name":"graph.windows.net",
"msgraph_host":"graph.microsoft.com",
"rbac_url":"https://pas.windows.net"}
So, I notice that the 'patient/.' and the 'launch' scope among a whole host of others that I have are not supported according to my openid config. The only supported ones are "openid","profile","email", "offline_access".
In Azure AD, 'App Registration' > 'Expose an API' I have a list of at least 15 scopes entered there. In 'API' permissions they are all listed there as well.
One other thing to note, AzureAD does not handle scopes with a forward slash. So, launch/patient has to be entered as launch-patient. We also had to implement a proxy server to capture the ~/oauth2/v2.0/authorize request and modify the scope parameter entries to reflect this before passing on the request to the actual server.
I guess the pertinent question now is: How do the scopes that I have entered manually get supported?

Google Admin Directory API, bad request 400 invalid_grant. (using service account)

So before showing my code, let me explain what steps I took to 'properly' set up service account environment.
In google developer console, created service account. (this produced Client ID (which is a long number), Service account (xxxxx#xxxx.iam.gserviceaccount.com), and private key which I downloaded in P12.
In Admin console, put the client ID with appropriate scope. In my case the scopes I added is https://www.googleapis.com/auth/admin.directory.group.readonly and https://www.googleapis.com/auth/admin.directory.group.member.readonly.
In my code, correctly set up private key path and other environments.
def getDirectoryService: Directory = {
val httpTransport: HttpTransport = new NetHttpTransport()
val jsonFactory: JacksonFactory = new JacksonFactory()
val credential: GoogleCredential = new GoogleCredential.Builder()
.setTransport(httpTransport)
.setJsonFactory(jsonFactory)
.setServiceAccountId("xxxxx#xxxx.iam.gserviceaccount.com")
.setServiceAccountScopes(util.Arrays.asList(DirectoryScopes.ADMIN_DIRECTORY_GROUP_READONLY, DirectoryScopes.ADMIN_DIRECTORY_GROUP_MEMBER_READONLY))
.setServiceAccountUser("admin#domain.com")
.setServiceAccountPrivateKeyFromP12File(
new java.io.File("/pathToKey/privatekey.p12"))
.build()
val service: Directory = new Directory.Builder(httpTransport, jsonFactory, null)
.setHttpRequestInitializer(credential).build()
service
}
And then I attempt to execute something like this:
service.groups().list().execute()
or
service.groups().list("domain.com").execute()
This code would result in,
com.google.api.client.auth.oauth2.TokenResponseException: 400 Bad Request
{
"error" : "invalid_grant"
}
at com.google.api.client.auth.oauth2.TokenResponseException.from(TokenResponseException.java:105)
at com.google.api.client.auth.oauth2.TokenRequest.executeUnparsed(TokenRequest.java:287)
at com.google.api.client.auth.oauth2.TokenRequest.execute(TokenRequest.java:307)
at com.google.api.client.googleapis.auth.oauth2.GoogleCredential.executeRefreshToken(GoogleCredential.java:384)
at com.google.api.client.auth.oauth2.Credential.refreshToken(Credential.java:489)
at com.google.api.client.auth.oauth2.Credential.intercept(Credential.java:217)
at com.google.api.client.http.HttpRequest.execute(HttpRequest.java:868)
at com.google.api.client.googleapis.services.AbstractGoogleClientRequest.executeUnparsed(AbstractGoogleClientRequest.java:419)
at com.google.api.client.googleapis.services.AbstractGoogleClientRequest.executeUnparsed(AbstractGoogleClientRequest.java:352)
at com.google.api.client.googleapis.services.AbstractGoogleClientRequest.execute(AbstractGoogleClientRequest.java:469)
at com.company.project.GoogleServiceProvider.getGroups(GoogleServiceProvider.scala:81)
at com.company.project.ProjectHandler.handle(ProjectHandler.scala:110)
at com.company.common.web.DispatcherServlet.service(DispatcherServlet.scala:40)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:790)
at org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:845)
at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:583)
at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:224)
at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1174)
at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:511)
at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:185)
at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1106)
at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:141)
at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:134)
at org.eclipse.jetty.server.Server.handle(Server.java:524)
at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:319)
at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:253)
at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:273)
at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:95)
at org.eclipse.jetty.io.SelectChannelEndPoint$2.run(SelectChannelEndPoint.java:93)
at org.eclipse.jetty.util.thread.strategy.ExecuteProduceConsume.executeProduceConsume(ExecuteProduceConsume.java:303)
at org.eclipse.jetty.util.thread.strategy.ExecuteProduceConsume.produceConsume(ExecuteProduceConsume.java:148)
at org.eclipse.jetty.util.thread.strategy.ExecuteProduceConsume.run(ExecuteProduceConsume.java:136)
at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:671)
at org.eclipse.jetty.util.thread.QueuedThreadPool$2.run(QueuedThreadPool.java:589)
at java.lang.Thread.run(Thread.java:745)
What could have I done wrong? I have been searching solution for past two days, and tried many things. One of the solutions I am not still not sure of is ntp syncing (as in how to exactly sync server time to ntp).
Any adivce would be very helpful, thank you!
UPDATE: I also made sure to activate the Admin Directory SDK, and enabled the Domain-Wide Delegation on developer's console.
UPDATE #2: I forgot to mention that, the admin account is not the owner of the project itself. So basically, I am a member of a domain, and I created a project, so I am the only owner of the project and the service account.(I am not the admin). But should an admin be owner of the project and create service account in order for this to work properly???
Ok, my problem was that in setServiceAccountUser I put admin group email address, not the actual user account. Apparently, it doesn't allow putting in group email (alias) address into setServiceAccountUser.
So after putting in an actual user account with admin privilege, it seems to be working.
I still wonder what would be the best practice though. As in, should I create a separate user account with admin privilege just for the project? I definitely don't want to just put in an admin account email address in my code.

Error on getting authorization URL more than once in Twitter4j

I'm using Twitter4j to implement the authorization workflow on my webapp (user acesses a page, twitter asks permission, I receive the callback and generate the oauth access token).
My first problem was that if I called a method to get the Twitter sigleton:
Twitter twitter = TwitterFactory.getSingleton();
twitter.setOAuthConsumer(getClientId(), getClientSecret());
1) Since OAuthConsumer would already be defined I would get an exception. And I can't find how to ask the singleton if it already has the credentials defined. What's the best way? My solution was to save the singleton in a private member...
2) Now I want to generate an AuthorizationURL, so I need to ask Twitter singleton the OAuthRequestToken:
RequestToken oauthRequestToken = twitter.getOAuthRequestToken(getCallbackURL()); //FIXME
And this throws an exception:
401:Authentication credentials (https://dev.twitter.com/pages/auth) were missing or incorrect. Ensure that you have set valid consumer key/secret, access token/secret, and the system clock is in sync.
message - Invalid or expired token.
code - 89
Relevant discussions can be found on the Internet at:
http://www.google.co.jp/search?q=3cc69290 or
http://www.google.co.jp/search?q=45a986a5
TwitterException{exceptionCode=[3cc69290-45a986a5], statusCode=401, message=Invalid or expired token., code=89, retryAfter=-1, rateLimitStatus=null, version=4.0.4}
at twitter4j.HttpClientImpl.handleRequest(HttpClientImpl.java:164)
at twitter4j.HttpClientBase.request(HttpClientBase.java:57)
at twitter4j.HttpClientBase.post(HttpClientBase.java:86)
at twitter4j.auth.OAuthAuthorization.getOAuthRequestToken(OAuthAuthorization.java:115)
at twitter4j.auth.OAuthAuthorization.getOAuthRequestToken(OAuthAuthorization.java:92)
at twitter4j.TwitterBaseImpl.getOAuthRequestToken(TwitterBaseImpl.java:292)
at twitter4j.TwitterBaseImpl.getOAuthRequestToken(TwitterBaseImpl.java:287)
(...)
Note: the 'Relevant discussions' links are not working as expected I think...
In short:
1) How can I ask the singleton if it already has the credentials defined in order to 'setOAuthConsumer' doesn't throw an error ?
2) How to re-ask the singleton to generate a new authorizationURL for the user to access and authorize (again) ?
Also posted in the corresponding forum
1) How can I ask the singleton if it already has the credentials defined in order to 'setOAuthConsumer' doesn't throw an error ?
There are a few ways that this can be done. You can set the oAuth consumer key and secret in a properties file named twitter4j.properties on your classpath. When you use the TwitterFactory, this is where the default properties come from.
If you want to set the values programmatically, the TwitterFactory also has a few overloaded constructors which allow this:
ConfigurationBuilder builder = new ConfigurationBuilder();
builder.setOAuthConsumerKey(CONSUMER_KEY);
builder.setOAuthConsumerSecret(CONSUMER_SECRET);
Configuration configuration = builder.build();
TwitterFactory factory = new TwitterFactory(configuration);
Twitter twitter = factory.getInstance();
2) How to re-ask the singleton to generate a new authorizationURL for the user to access and authorize (again) ?
I assume that your requirement is to have the user authorize every time. If this is the case, this is handled via Twitters API. There are 2 oAuth endpoints https://api.twitter.com/oauth/authenticate and https://api.twitter.com/oauth/authorize. The authenticate endpoint is the normal Sign in with Twitter functionality where the user will approve once and then automatically logged in every time after. The authorize endpoint will require authorization every time.
Using Twitter4j, these are separate methods that can be called on your RequestToken. You redirect to the appropriate URL based on your requirement.
The solution I've found is presented here:
Twitter instance = new TwitterFactory().getInstance();
instance.setOAuthConsumer(getClientId(), getClientSecret());
RequestToken requestToken = new RequestToken(getOauthToken(),getOauthTokenSecret());
AccessToken oAuthAccessToken = instance.getOAuthAccessToken(requestToken, oauthVerifier);
requestTokenand oauthVerifier are received as parameters in the callback. getOauthToken() and getOauthTokenSecret() retrieve the tokens retrieved by the library in the first step and that were saved in a cache (user -> tokens).
Inspired by this question/answers: Having multiple Twitter instances with twitter4j library.

Gmail API returns 403 error code and "Delegation denied for <user email>"

Gmail API fails for one domain when retrieving messages with this error:
com.google.api.client.googleapis.json.GoogleJsonResponseException: 403 OK
{
"code" : 403,
"errors" : [ {
"domain" : "global",
"message" : "Delegation denied for <user email>",
"reason" : "forbidden"
} ],
"message" : "Delegation denied for <user email>"
}
I am using OAuth 2.0 and Google Apps Domain-Wide delegation of authority to access the user data. The domain has granted data access rights to the application.
Seems like best thing to do is to just always have userId="me" in your requests. That tells the API to just use the authenticated user's mailbox--no need to rely on email addresses.
I had the same issue before, the solution is super tricky, you need to impersonate the person you need to access gmail content first, then use userId='me' to run the query. It works for me.
here is some sample code:
users = # coming from directory service
for user in users:
credentials = service_account.Credentials.from_service_account_file(
SERVICE_ACCOUNT_FILE, scopes=SCOPES)
####IMPORTANT######
credentials_delegated = credentials.with_subject(user['primaryEmail'])
gmail_service = build('gmail', 'v1', credentials=credentials_delegated)
results = gmail_service.users().labels().list(userId='me').execute()
labels = results.get('labels', [])
for label in labels:
print(label['name'])
Our users had migrated into a domain and their account had aliases attached to it. We needed to default the SendAs address to one of the imported aliases and want a way to automate it. The Gmail API looked like the solution, but our privileged user with roles to make changes to the accounts was not working - we kept seeing the "Delegation denied for " 403 error.
Here is a PHP example of how we were able to list their SendAs settings.
<?PHP
//
// Description:
// List the user's SendAs addresses.
//
// Documentation:
// https://developers.google.com/gmail/api/v1/reference/users/settings/sendAs
// https://developers.google.com/gmail/api/v1/reference/users/settings/sendAs/list
//
// Local Path:
// /path/to/api/vendor/google/apiclient-services/src/Google/Service/Gmail.php
// /path/to/api/vendor/google/apiclient-services/src/Google/Service/Gmail/Resource/UsersSettingsSendAs.php
//
// Version:
// Google_Client::LIBVER == 2.1.1
//
require_once $API_PATH . '/path/to/google-api-php-client/vendor/autoload.php';
date_default_timezone_set('America/Los_Angeles');
// this is the service account json file used to make api calls within our domain
$serviceAccount = '/path/to/service-account-with-domain-wide-delagation.json';
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $serviceAccount );
$userKey = 'someuser#my.domain';
// In the Admin Directory API, we may do things like create accounts with
// an account having roles to make changes. With the Gmail API, we cannot
// use those accounts to make changes. Instead, we impersonate
// the user to manage their account.
$impersonateUser = $userKey;
// these are the scope(s) used.
define('SCOPES', implode(' ', array( Google_Service_Gmail::GMAIL_SETTINGS_BASIC ) ) );
$client = new Google_Client();
$client->useApplicationDefaultCredentials(); // loads whats in that json service account file.
$client->setScopes(SCOPES); // adds the scopes
$client->setSubject($impersonateUser); // account authorized to perform operation
$gmailObj = new Google_Service_Gmail($client);
$res = $gmailObj->users_settings_sendAs->listUsersSettingsSendAs($userKey);
print_r($res);
?>
I wanted to access the emails of fresh email id/account but what happened was, the recently created folder with '.credentials' containing a JSON was associated with the previous email id/account which I tried earlier. The access token and other parameters present in JSON are not associated with new email id/account. So, in order make it run you just have to delete the '.credentails' folder and run the program again. Now, the program opens the browser and asks you to give permissions.
To delete the folder containing files in python
import shutil
shutil.rmtree("path of the folder to be deleted")
you may add this at the end of the program
Recently I started exploring Gmail API and I am following the same approach as Guo mentioned. However, it is going to take of time and too many calls when we the number of users or more. After domain wide delegation my expectation was admin id will be able to access the delegated inboxes, but seems like we need to create service for each user.

username is null in DotNetOpenAuth2.ResourceServer.VerifyAccess

I have implemented an authorization server based on the sample and am receiving an access token in response to client credentials request. From my understanding this access token has a null username because it is not tied to a user.
I have implemented a resource server also based on the sample. When I try to validate the access token in my wcf server (resource server) in OAuthAuthorizationManager.VerifyOAuth2 I get an ArgumentNullException for username from
var error = resourceServer.VerifyAccess(httpRequestInfo, out result);
How can I modify OAuthAuthorizationManager to allow a null username?
Do I create a generic principal on the fly and assign it to the scope in the token.
i.e. should I use
var error = resourceServer.VerifyAccess(httpRequestInfo, out userName, out scope);
instead?
This is an issue with DotNetOpenAuth v4.0. v4.1 has this issue fixed. It's not released yet, but you can snag a copy from NuGet if you point it at this channel:
http://teamcity.dotnetopenauth.net:82/guestAuth/app/nuget/v1/FeedService.svc

Resources