diff --git a/cloning-instance-with-rest/README.md b/cloning-instance-with-rest/README.md
index 18d723a..900f85b 100644
--- a/cloning-instance-with-rest/README.md
+++ b/cloning-instance-with-rest/README.md
@@ -88,6 +88,22 @@ Working with the CLI, complete the following steps.
target_region |
The region that the instance of App ID that you want to copy the configuration to is located in. If not provided, it will default to the source region. |
+
+ | attr_name |
+ (Optional)When migrating users, only migrate users that have a matching attribute name and value pair. |
+
+
+ | attr_value |
+ (Optional)When migrating users, only migrate users that have a matching attribute name and value pair. |
+
+
+ | clientid |
+ (Optional)When migrating user profiles, the client ID of the instance of App ID that you want to copy the user profiles to. |
+
+
+ | secret |
+ (Optional) When migrating user profiles, the client secret of the instance of App ID that you want to copy the user profiles to. |
+
>Tip: To see the REST messages, append the `-v` flag to the command.
@@ -100,6 +116,21 @@ Example:
appidc xxxxxxx yyyyyyy --apikey KkKkKkKkkKkKkKkKk --region eu-gb
```
+As another example, you can choose to migrate only specific users by providing a user profile attribute name and value. Only users with a matching attribute name and value will be migrated.
+
+Example:
+```
+appidc xxxxxxx yyyyyyy --apikey KkKkKkKkkKkKkKkKk --region eu-gb -attr_name user-status -attr_value active
+```
+
+If you wish to also migrate user profiles, you must also specify a valid client ID and secret of a credential on the target App ID instance.
+Note that users that are migrated will automatically be requested to reset their password.
+Example:
+```
+appidc xxxxxxx yyyyyyy --apikey KkKkKkKkkKkKkKkKk --region eu-gb -clientid ccccccccc -secret ssssssssssssss
+```
+
+
And that's it!
To go even further with App ID and access management, check out [our documentation](https://console.bluemix.net/docs/services/appid/api-reference.html)!
diff --git a/cloning-instance-with-rest/appid_instance_copy/copy_config.py b/cloning-instance-with-rest/appid_instance_copy/copy_config.py
index 040dd32..eb93c01 100755
--- a/cloning-instance-with-rest/appid_instance_copy/copy_config.py
+++ b/cloning-instance-with-rest/appid_instance_copy/copy_config.py
@@ -2,23 +2,41 @@
import requests
import json
import argparse
+import base64
+import urllib
+import random
+import sys
-
-
-def get_from_api(path, token):
+def get_from_api(path, token, isConfig=True, tenantId=None):
+ if tenantId is None:
+ tenantId = src_tenantId
+
headers = {'Authorization': token, 'Accept': 'application/json'}
- url = src_management_url + src_tenantId + "/config/" +path
+ configPath = "/config" if isConfig else ""
+ url = src_management_url + tenantId + configPath + "/" +path
+
return requests.get(
url,
- headers=headers);
+ headers=headers);
-def put_to_api(path, content, token):
+def put_to_api(path, content, token, isConfig=True):
headers = {'Authorization': token, 'Accept': 'application/json', 'Content-Type': 'application/json'}
- url = tgt_management_url + trgt_tenantId + "/config/" + path
+ configPath = "/config" if isConfig else ""
+ url = tgt_management_url + trgt_tenantId + configPath + "/" + path
return requests.put(
url,
data=content,
headers=headers);
+
+def post_to_api(path, content, token, isConfig=True, isJson=True):
+ contentType = "application/json" if isJson else "application/x-www-form-urlencoded"
+ headers = {'Authorization': token, 'Accept': 'application/json', 'Content-Type': contentType}
+ configPath = "/config" if isConfig else ""
+ url = tgt_management_url + trgt_tenantId + configPath + "/" + path
+ return requests.post(
+ url,
+ data=content,
+ headers=headers);
def get_iam_token():
headers = {'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json'}
@@ -26,13 +44,27 @@ def get_iam_token():
r = requests.post(iam_url + ".bluemix.net/oidc/token", data=data, headers=headers);
return 'Bearer ' + json.loads(r.text)['access_token'];
+
+def get_user_token(loginId, password):
+
+ basicAuthHeader = 'Basic ' + str(base64.b64encode(bytes(clientId + ":" + clientSecret)))
+ headers = {'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json', 'Authorization': basicAuthHeader}
+ data = 'grant_type=password&username=' + urllib.quote(loginId) + '&password=' + password;
+
+ r = requests.post(tgt_oauth_url, data=data, headers=headers);
+ accessToken = json.loads(r.text).get('access_token', None)
+ if accessToken is None:
+ print("Couldn't get user access token - " + str(r.content))
+ return
+
+ return 'Bearer ' + accessToken
-def copy(path, token):
- r = get_from_api(path, token)
+def copy(path, token, isConfig=True):
+ r = get_from_api(path, token, isConfig)
debug(r.status_code)
debug(r.content)
if 200 <= r.status_code < 300:
- print("success! got " + path + " from source")
+ print("Success! got " + path + " from source")
else:
print("Failed to get " + path + " from source")
jcontent = r.content
@@ -41,7 +73,7 @@ def copy(path, token):
debug(r.status_code)
debug(r.text)
if 200 <= r.status_code < 300:
- print("success! put to " + path + " at target")
+ print("Success! put to " + path + " at target")
else:
print("Failed to put to " + path + " at target")
@@ -54,10 +86,108 @@ def copyTemplates(token):
def copyActions(token):
copy("cloud_directory/action_url/" + "on_user_verified", token)
copy("cloud_directory/action_url/" + "on_reset_password", token)
+
+def copyCloudUsers(token):
+ path = "cloud_directory/Users"
+ users = get_from_api(path, token, False)
+ data = json.loads(users.content)
+ resources = data['Resources']
+ for user in resources:
+ debug("Processing user " + user['displayName'])
+ loginId = user['emails'][0]['value']
+ userProfile = get_user_profile(loginId, token)
+ if is_user_attribute_filter():
+ userProfileValue = None
+ if userProfile and userProfile['attributes']:
+ userProfileValue = userProfile['attributes'].get(attrName, None)
+
+ if not userProfileValue or not userProfileValue == attrValue:
+ print("Skipping user with incorrect " + attrName + " : is " + str(userProfileValue) + " expected " + attrValue)
+ continue
+
+ user['password'] = str(random.randint(1, sys.maxsize))
+ r = post_to_api(path, json.dumps(user), token, False)
+ debug(r.status_code)
+ debug(r.text)
+ if 200 <= r.status_code < 300:
+ print("Success! put to " + path + " at target")
+
+ if is_copy_user_profile():
+ copy_user_profile(userProfile, loginId, user['password'], token)
+
+ request_password_reset(loginId, token)
+
+ else:
+ print("Failed to put to " + path + " at target: " + str(r.status_code))
+
+def get_user_profile_id(loginId, token, tenantId=None):
+
+ user = get_from_api("users?email="+urllib.quote(loginId), token, False, tenantId)
+ userData = json.loads(user.content)['users']
+
+ if not userData:
+ return
+
+ return userData[0]['id']
+
+def get_user_profile(loginId, token):
+ userProfileId = get_user_profile_id(loginId, token)
+
+ if (userProfileId is not None):
+ profile = get_from_api("users/" + userProfileId + "/profile", token, False)
+ return json.loads(profile.content)
+
+ return
+def is_user_attribute_filter():
+ return (attrName is not None) and (attrValue is not None)
+
+def is_copy_user_profile():
+ return (clientId is not None) and (clientSecret is not None)
+
+def copy_user_profile(userProfile, loginId, password, token):
+ if not userProfile or not userProfile['attributes']:
+ return
+
+ perform_user_login(loginId, password)
+
+ #must retrieve the target user profile id
+ targetProfileId = get_user_profile_id(loginId, token, trgt_tenantId)
+ if targetProfileId is None:
+ print("Couldn't find target profile for user " + loginId)
+ return
+
+ path = "users/" + targetProfileId + "/profile"
+ r = put_to_api(path, json.dumps(userProfile), token, False)
+ debug(r.status_code)
+ debug(r.text)
+ if 200 <= r.status_code < 300:
+ print("Success! put to " + path + " at target")
+ else:
+ print("Failed to put to " + path + " at target: " + str(r.status_code))
+
+#User login is currently required to initialize a new user's profile.
+#This must be conducted prior to adding profile attributes.
+def perform_user_login(loginId, password):
+ userToken = get_user_token(loginId, password)
+
+def request_password_reset(loginId, token):
+ path = "cloud_directory/forgot_password"
+ params = "email=" + urllib.quote(loginId)
+ r = post_to_api(path, params, token, False, False)
+ debug(r.status_code)
+ debug(r.text)
+
+ if 200 <= r.status_code < 300:
+ debug("Success! post to " + path + " at target")
+ else:
+ print("Failed to post to " + path + " at target: " + str(r.status_code))
+
def debug(str):
if verbose:
print(str)
+
+
def main():
parser = argparse.ArgumentParser(description='Copy configuration from one App ID instance to another')
@@ -78,6 +208,16 @@ def main():
parser.add_argument('-v', '--verbose', action='store_true',
help='Run with verbose mode. REST messages content will be displayed')
+
+ parser.add_argument('-a', '--attr_name', type=str,
+ help='User profile attribute name for user selection')
+ parser.add_argument('-l', '--attr_value', type=str,
+ help='User profile attribute value for user selection')
+
+ parser.add_argument('-c', '--clientid', type=str,
+ help='Client ID for user profile migration')
+ parser.add_argument('-s', '--secret', type=str,
+ help='Client secret for user profile migration')
args = parser.parse_args()
@@ -88,7 +228,12 @@ def main():
global iam_url
global src_management_url
global tgt_management_url
+ global tgt_oauth_url
global verbose
+ global attrName
+ global attrValue
+ global clientId
+ global clientSecret
src_tenantId = args.source
trgt_tenantId = args.target
@@ -96,6 +241,10 @@ def main():
region = args.region
tgt_region = args.target_region
verbose = args.verbose
+ attrName = args.attr_name
+ attrValue = args.attr_value
+ clientId = args.clientid
+ clientSecret = args.secret
if region == "us-south":
region = 'ng'
@@ -109,16 +258,21 @@ def main():
iam_url = "https://iam." + region
src_management_url = "https://appid-management." + region + ".bluemix.net/management/v4/"
tgt_management_url = "https://appid-management." + tgt_region + ".bluemix.net/management/v4/"
+ tgt_oauth_url = "https://appid-oauth." + tgt_region + ".bluemix.net/oauth/v3/" + trgt_tenantId + "/token"
+
debug("source:" + src_management_url)
debug("target:" + tgt_management_url)
+ debug("target oauth:" + tgt_oauth_url)
+ debug("attrName:" + str(attrName))
+ debug("attrValue:" + str(attrValue))
token = get_iam_token()
- copy("idps/facebook", token)
- copy("idps/google", token)
+ #copy("idps/facebook", token)
+ #copy("idps/google", token)
copy("idps/cloud_directory", token)
# copy("idps/saml", token)
- #
+
copy("tokens", token)
# copy("redirect_uris", token)
copy("users_profile", token)
@@ -128,7 +282,8 @@ def main():
copyTemplates(token)
#copyActions(token)
-
+
+ copyCloudUsers(token)
if __name__ == "__main__":
diff --git a/openidauth/.gitignore b/openidauth/.gitignore
new file mode 100644
index 0000000..3bb19ff
--- /dev/null
+++ b/openidauth/.gitignore
@@ -0,0 +1 @@
+.gradle/
diff --git a/openidauth/OpenidAuth-ear/.gitignore b/openidauth/OpenidAuth-ear/.gitignore
new file mode 100644
index 0000000..567609b
--- /dev/null
+++ b/openidauth/OpenidAuth-ear/.gitignore
@@ -0,0 +1 @@
+build/
diff --git a/openidauth/OpenidAuth-ear/build.gradle b/openidauth/OpenidAuth-ear/build.gradle
new file mode 100644
index 0000000..dd79848
--- /dev/null
+++ b/openidauth/OpenidAuth-ear/build.gradle
@@ -0,0 +1,26 @@
+apply plugin: 'ear'
+apply plugin: 'java'
+apply plugin: 'maven-publish'
+
+dependencies {
+ deploy project(path: ':OpenidAuth-war', configuration: 'archives')
+}
+
+ear {
+ baseName = 'OpenidAuth'
+ deploymentDescriptor {
+ applicationName = "OpenidAuth"
+ initializeInOrder = true
+ description = "An application for custom integration with OAuth2 Identity Providers."
+ }
+}
+
+publishing {
+ publications {
+ mavenJava(MavenPublication) {
+ groupId 'com.ibm.openid'
+ artifactId 'OpenidAuth'
+ artifact ear
+ }
+ }
+}
diff --git a/openidauth/OpenidAuth-war/.gitignore b/openidauth/OpenidAuth-war/.gitignore
new file mode 100644
index 0000000..77474d4
--- /dev/null
+++ b/openidauth/OpenidAuth-war/.gitignore
@@ -0,0 +1,5 @@
+bin/
+build/
+.settings/
+.classpath
+.project
diff --git a/openidauth/OpenidAuth-war/build.gradle b/openidauth/OpenidAuth-war/build.gradle
new file mode 100644
index 0000000..60a3502
--- /dev/null
+++ b/openidauth/OpenidAuth-war/build.gradle
@@ -0,0 +1,22 @@
+apply plugin: 'war'
+apply plugin: 'eclipse'
+
+war {
+ archiveName = 'OpenidAuth.war'
+}
+
+dependencies {
+ compile('javax.servlet:javax.servlet-api:3.1.0')
+ compile('javax.ws.rs:javax.ws.rs-api:2.0.1')
+ compile('net.sf.ehcache:ehcache-core:2.6.11')
+ compile('org.glassfish.hk2:hk2-api:2.5.0-b05')
+ compile('com.fasterxml.jackson.core:jackson-annotations:2.9.5')
+ compile('com.fasterxml.jackson.core:jackson-core:2.9.5')
+ compile('com.fasterxml.jackson.core:jackson-databind:2.9.5')
+ compile('com.fasterxml.jackson.jaxrs:jackson-jaxrs-base:2.9.5')
+ compile('com.fasterxml.jackson.jaxrs:jackson-jaxrs-json-provider:2.9.5')
+ compile('org.glassfish.jersey.core:jersey-client:2.6')
+ compile('org.glassfish.jersey.core:jersey-common:2.6')
+ compile('org.slf4j:slf4j-api:1.7.25')
+ compile('org.slf4j:slf4j-jdk14:1.7.25')
+}
diff --git a/openidauth/OpenidAuth-war/src/main/java/com/ibm/openid/authentication/AuthServlet.java b/openidauth/OpenidAuth-war/src/main/java/com/ibm/openid/authentication/AuthServlet.java
new file mode 100644
index 0000000..cd33365
--- /dev/null
+++ b/openidauth/OpenidAuth-war/src/main/java/com/ibm/openid/authentication/AuthServlet.java
@@ -0,0 +1,751 @@
+/*
+ * Copyright 2018 IBM Corp.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.ibm.openid.authentication;
+
+import com.fasterxml.jackson.core.JsonParseException;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.ibm.openid.authentication.exception.RequestException;
+import com.ibm.openid.authentication.exception.ValidationException;
+import com.ibm.openid.authentication.rest.Attributes;
+import com.ibm.openid.authentication.rest.JwtToken;
+import com.ibm.openid.authentication.rest.RestClientBuilderUtility;
+import com.ibm.openid.authentication.rest.RestUtils;
+import com.ibm.openid.authentication.rest.UriUtils;
+import com.ibm.openid.authentication.rest.Users;
+import com.ibm.openid.cache.TokenCache;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URLEncoder;
+import java.util.Base64;
+import java.util.ResourceBundle;
+import java.util.UUID;
+import javax.servlet.ServletException;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.client.Invocation;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedHashMap;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A simple servlet that performs form authentication with an OAuth2 IdP such as
+ * AppID.
+ */
+public class AuthServlet extends HttpServlet {
+ private static final Logger log = LoggerFactory.getLogger(AuthServlet.class);
+
+ private static final String OIDC_CLIENT_CONTEXT_PATH = "/oidcclient";
+
+ private static final String PROPERTIES_FILE_NAME = "com.ibm.openid.authentication.auth";
+ private static final String PROP_USER_STATUS_EXPECTED = "user.status.expected";
+ private static final String PROP_USER_STATUS_ATTRIBUTE = "user.status.attribute";
+ private static final String PROP_ENVIRONMENT = "environment";
+ private static final String PROP_REDIRECT_URI_WHITELIST = "security.redirect.uri.whitelist";
+
+ private static final String APPID_GRANT_TYPE = "password";
+ private static final String APPID_SCOPES = "openid%20profile%20email";
+
+ private static final String PROP_APPID_API_KEY = "appid.api.key";
+ private static final String APPID_IDENTITY_GRANT_TYPE = "urn:ibm:params:oauth:grant-type:apikey";
+
+ private static final String WAS_STATE_COOKIE_PREFIX = "WASOidcState";
+ private static final String WAS_REQ_URL_COOKIE_PREFIX = "WASReqURLOidc";
+
+ private static final String AUTH_RESULT_MSG_ID = "OIAT10000";
+ private static final String AUTH_RESULT_SUCCESS = "success";
+ private static final String AUTH_RESULT_FAIL = "fail";
+ private static final String AUTH_RESULT_INVALID = "invalid";
+ private static final String AUTH_RESULT_SEPARATOR = "|";
+
+ protected static final ObjectMapper mapper = new ObjectMapper();
+
+ private static final String LOGIN_JSP = "/login.jsp";
+
+ @Override
+ protected void doGet(final HttpServletRequest req,
+ final HttpServletResponse resp) throws ServletException, IOException {
+
+ this.forwardToLogin(req, resp, req.getAttribute("tenantId").toString(),
+ req.getParameter("client_id"), getRedirectUri(req),
+ req.getParameter("state"), null);
+ }
+
+ @Override
+ protected void doPost(final HttpServletRequest req,
+ final HttpServletResponse resp) throws ServletException, IOException {
+
+ doCodeFlowLogin(req, resp);
+ }
+
+ /**
+ * Uses the oauth2 authorization code flow for login. This involves invoking
+ * the authorization endpoint of the IdP, then redirecting back to the given
+ * OIDC redirect application.
+ *
+ * @param req
+ * request
+ * @param resp
+ * response
+ * @throws JsonParseException
+ * @throws JsonMappingException
+ * @throws IOException
+ * @throws ServletException
+ */
+ private void doCodeFlowLogin(final HttpServletRequest req,
+ final HttpServletResponse resp) throws JsonParseException,
+ JsonMappingException, IOException, ServletException {
+ final String email = req.getParameter("email");
+ final String password = req.getParameter("password");
+ final String clientId = req.getParameter("clientId");
+ final String tenantId = req.getAttribute("tenantId").toString();
+ final String state = req.getParameter("state");
+ final String stateCookie = req.getParameter("stateCookie");
+ final String redirectUri = getRedirectUri(req);
+ final String protectedUrl = getWasReqUrl(req, stateCookie);
+ String authResult = null;
+ String authMsg = null;
+
+ try {
+ if (isLibertyOidcClientUri(redirectUri)
+ && !isStateCookiePresent(req, stateCookie)) {
+ // the oidcclient will reject the request - redirect the user back to
+ // login with a fresh state instead
+ log.debug("AuthServlet state cookie not present, redirecting back to "
+ + protectedUrl);
+ authResult = AUTH_RESULT_INVALID;
+ authMsg = "missing state cookie";
+ if (protectedUrl != null) {
+ resp.sendRedirect(protectedUrl);
+ } else {
+ // no other choice but to fail hard
+ resp.sendError(400, "missing header");
+ }
+
+ return;
+ }
+
+ final String oidcAppUri = codeFlowAuthenticate(tenantId, clientId, email,
+ password, state, redirectUri);
+
+ resp.sendRedirect(oidcAppUri);
+ authResult = AUTH_RESULT_SUCCESS;
+ return;
+
+ } catch (final ValidationException e) { // NOSONAR
+ // login was not successful
+ forwardToLogin(req, resp, tenantId, clientId, redirectUri, state,
+ e.getDescription());
+ authResult = AUTH_RESULT_FAIL;
+ authMsg = e.getMessage();
+ } finally {
+ logAuthResult(tenantId, clientId, email, protectedUrl, authResult,
+ authMsg);
+ }
+ }
+
+ /**
+ * Return true if and only if this uri is for Liberty's oidcclient
+ * application.
+ *
+ * @param url
+ * url
+ * @return true if the url request
+ */
+ private boolean isLibertyOidcClientUri(final String url) {
+ if (url != null) {
+ return url.toLowerCase().contains(OIDC_CLIENT_CONTEXT_PATH);
+ }
+
+ return false;
+ }
+
+ /**
+ * Check for the presence of the WAS OIDC state cookie.
+ *
+ * @param req
+ * request
+ * @param cookieName
+ * cookie name
+ * @return true if the cookie is present
+ */
+ private boolean isStateCookiePresent(final HttpServletRequest req,
+ final String cookieName) {
+ return getStateCookie(req, cookieName) != null;
+ }
+
+ /**
+ * Check for the presence of the WAS OIDC state cookie, by prefix.
+ *
+ * @param req
+ * request
+ * @return cookie if the cookie is present
+ */
+ private Cookie getStateCookie(final HttpServletRequest req) {
+ if (req.getCookies() == null) {
+ return null;
+ }
+
+ for (final Cookie cookie : req.getCookies()) {
+ if (cookie.getName().startsWith(WAS_STATE_COOKIE_PREFIX)) {
+ return cookie;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Check for the presence of the WAS OIDC state cookie, by exact name.
+ *
+ * @param req
+ * request
+ * @param cookieName
+ * cookie name
+ * @return cookie if the cookie is present
+ */
+ private Cookie getStateCookie(final HttpServletRequest req,
+ final String cookieName) {
+ if (cookieName == null || req.getCookies() == null) {
+ return null;
+ }
+ for (final Cookie cookie : req.getCookies()) {
+ if (cookie.getName().equals(cookieName)) {
+ return cookie;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Retrieve the request URL from the WAS ReqUrl cookie.
+ *
+ * @param req
+ * request
+ * @param stateCookie
+ * state cookie name
+ * @return url
+ * @throws RequestException
+ */
+ private String getWasReqUrl(final HttpServletRequest req,
+ final String stateCookie) throws RequestException {
+ if (stateCookie == null || req.getCookies() == null) {
+ // we can't determine the was req url
+ return null;
+ }
+ final String nonce = stateCookie.length() >= WAS_STATE_COOKIE_PREFIX
+ .length() ? stateCookie.substring(WAS_STATE_COOKIE_PREFIX.length())
+ : null;
+
+ for (final Cookie cookie : req.getCookies()) {
+ if (cookie.getName().equals(WAS_REQ_URL_COOKIE_PREFIX + nonce)) {
+ final String reqUrl = cookie.getValue();
+ validateWasReqUrl(reqUrl);
+ return reqUrl;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Forward this request and common parameters to the login page
+ *
+ * @param req
+ * request
+ * @param resp
+ * response
+ * @param tenantId
+ * tenant ID
+ * @param clientId
+ * client ID
+ * @param redirectUri
+ * redirect URI
+ * @param state
+ * application state
+ * @param loginMessage
+ * message for the login page
+ * @throws ServletException
+ * @throws IOException
+ */
+ private void forwardToLogin(final HttpServletRequest req,
+ final HttpServletResponse resp, final String tenantId,
+ final String clientId, final String redirectUri, final String state,
+ final String loginMessage) throws ServletException, IOException {
+
+ req.setAttribute("loginMessage", loginMessage);
+ req.setAttribute("tenantId", tenantId);
+ req.setAttribute("clientId", clientId);
+ req.setAttribute("redirectUri", redirectUri);
+ req.setAttribute("state", state);
+ req.setAttribute("environment",
+ getProperties().getString(PROP_ENVIRONMENT));
+ final Cookie stateCookie = getStateCookie(req);
+ if (stateCookie != null) {
+ req.setAttribute("stateCookie", stateCookie.getName());
+ }
+ req.getServletContext().getRequestDispatcher(LOGIN_JSP).forward(req, resp);
+ }
+
+ /**
+ * Authenticate with a given user and password via the oauth2 authorization
+ * code flow, returning the oidcApp redirect URI. The returned uri is expected
+ * to contain the code and state parameters.
+ *
+ * @param tenantId
+ * tenant ID
+ * @param clientId
+ * client ID
+ * @param email
+ * email
+ * @param password
+ * password
+ * @param state
+ * state
+ * @param redirectUri
+ * redirect URI
+ * @return oidcApp redirect URI
+ * @throws JsonParseException
+ * @throws JsonMappingException
+ * @throws IOException
+ * @throws ValidationException
+ * @throws RequestException
+ */
+ private String codeFlowAuthenticate(final String tenantId,
+ final String clientId, final String email, final String password,
+ final String state, final String redirectUri) throws JsonParseException,
+ JsonMappingException, IOException, ValidationException, RequestException {
+
+ // obtain a JWT
+ final JwtToken token = this.getIdToken(tenantId, clientId, email, password);
+
+ // enrich with tenant and clientId
+ token.setTenantId(tenantId);
+ token.setClientId(clientId);
+
+ // generate a UUID and persist with JWT
+ final String uuid = UUID.randomUUID().toString().replace("-", "");
+ new TokenCache().put(uuid, token);
+
+ log.debug("AuthServlet generated a token for uuid " + uuid);
+
+ return redirectUri + "?code=" + uuid + "&state=" + state;
+ }
+
+ /**
+ * Retrieve a new Json Web Token.
+ *
+ * @param tenantId
+ * tenant ID
+ * @param clientId
+ * client ID
+ * @param email
+ * email
+ * @param password
+ * password
+ * @return JWT
+ * @throws JsonParseException
+ * @throws JsonMappingException
+ * @throws IOException
+ * @throws ValidationException
+ * @throws RequestException
+ */
+ private JwtToken getIdToken(final String tenantId, final String clientId,
+ final String email, final String password) throws JsonParseException,
+ JsonMappingException, IOException, ValidationException, RequestException {
+ final URI uri = UriUtils.getOauthTokenUri(tenantId);
+
+ final String clientSecret = getClientSecret(clientId);
+
+ final String authHeader = new String(
+ Base64.getEncoder().encode((clientId + ":" + clientSecret).getBytes()))
+ .replaceAll("\\s", "").replaceAll("\\n", "");
+
+ final String body = "grant_type=" + APPID_GRANT_TYPE + "&username="
+ + URLEncoder.encode(email, "UTF-8") + "&password=" + password
+ + "&scope=" + APPID_SCOPES;
+
+ final MultivaluedMap headers = new MultivaluedHashMap();
+ headers.putSingle("Authorization", "Basic " + authHeader);
+ final Response tokenResponse = post(uri, body,
+ MediaType.APPLICATION_FORM_URLENCODED_TYPE, headers);
+ final String responseStr = tokenResponse.readEntity(String.class);
+ final JwtToken token = mapper.readValue(responseStr, JwtToken.class);
+ if (token == null) {
+ log.info(
+ "AuthServlet login token json was null, response: " + responseStr);
+ throw new ValidationException("empty_token",
+ "Oops, the account details you entered were not recognized, please try again.");
+ }
+
+ if (token.getAccessToken() == null) {
+ log.debug(
+ "AuthServlet login wasn't successful, response: " + responseStr);
+ if (token.getErrorDescription().equals("Pending user verification")) {
+ throw new ValidationException(token.getErrorDescription(),
+ "You must verify your account prior to login, please check your e-mail.");
+ } else {
+ // general login failure
+ throw new ValidationException(token.getErrorDescription(),
+ "Oops, the account details you entered were not recognized, please try again.");
+ }
+ }
+
+ final String expectedStatus = getProperties()
+ .getString(PROP_USER_STATUS_EXPECTED);
+ if (!expectedStatus.isEmpty()) {
+ final String userStatus = getUserStatus(tenantId, clientId, email);
+ if (userStatus == null || !userStatus.equalsIgnoreCase(expectedStatus)) {
+ log.info("AuthServlet login failed: user status is '" + userStatus
+ + "', expected '" + expectedStatus + "'");
+ throw new ValidationException("invalid_user_status",
+ "User account is not enabled, please contact an administrator");
+ } else {
+ log.debug("AuthServlet login user status check passed: user " + email
+ + " status is '" + userStatus + "', expected '" + expectedStatus
+ + "'");
+ }
+ }
+
+ return token;
+
+ }
+
+ /**
+ * Retrieve the status of the user with the given email.
+ *
+ * @param tenantId
+ * tenant
+ * @param clientId
+ * client
+ * @param email
+ * email
+ * @return user status
+ * @throws JsonParseException
+ * @throws JsonMappingException
+ * @throws IOException
+ */
+ private String getUserStatus(final String tenantId, final String clientId,
+ final String email)
+ throws JsonParseException, JsonMappingException, IOException {
+ final MultivaluedMap headersWithIamToken = this
+ .getHeadersWithIamToken();
+ final String userId = getUserId(email, tenantId, headersWithIamToken);
+
+ if (userId != null) {
+ final Attributes profile = getProfile(userId, tenantId,
+ headersWithIamToken);
+ if (profile != null) {
+ return profile.getAttributes()
+ .get(getProperties().getString(PROP_USER_STATUS_ATTRIBUTE));
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Retrieve the profile's map of attributes.
+ *
+ * @param userId
+ * user ID
+ * @param tenantId
+ * tenant ID
+ * @param headersWithIamToken
+ * headers
+ * @return attributes
+ * @throws JsonParseException
+ * @throws JsonMappingException
+ * @throws IOException
+ */
+ private Attributes getProfile(final String userId, final String tenantId,
+ final MultivaluedMap headersWithIamToken)
+ throws JsonParseException, JsonMappingException, IOException {
+ final Response userResponse = get(
+ UriUtils.getUserProfileURI(userId, tenantId), headersWithIamToken);
+
+ final String responsePayload = userResponse.readEntity(String.class);
+ final Attributes attributes = mapper.readValue(responsePayload,
+ Attributes.class);
+ return attributes;
+ }
+
+ /**
+ * Retrieve the AppID user ID of the given login ID.
+ *
+ * @param loginID
+ * login ID
+ * @param tenantId
+ * tenant ID
+ * @param headersWithIamToken
+ * request headers
+ * @return user ID
+ * @throws IOException
+ * @throws JsonMappingException
+ * @throws JsonParseException
+ */
+ private String getUserId(final String loginID, final String tenantId,
+ final MultivaluedMap headersWithIamToken)
+ throws JsonParseException, JsonMappingException, IOException {
+ final Response userResponse = get(
+ UriUtils.getUserQueryURI(loginID, tenantId), headersWithIamToken);
+
+ final String responsePayload = userResponse.readEntity(String.class);
+ final Users users = mapper.readValue(responsePayload, Users.class);
+ if (users.getUsers().size() > 0) {
+ return users.getUsers().get(0).getId();
+ }
+
+ return null;
+ }
+
+ /**
+ * Retrieve headers with an IAM token.
+ *
+ * @return headers
+ * @throws IOException
+ */
+ private MultivaluedMap getHeadersWithIamToken()
+ throws IOException {
+ final String iamAccessToken = getIamToken().getAccessToken();
+ final MultivaluedMap headersWithIamToken = new MultivaluedHashMap();
+ headersWithIamToken.putSingle("Authorization", "Bearer " + iamAccessToken);
+
+ return headersWithIamToken;
+ }
+
+ /**
+ * Issue a post request to the given URI.
+ *
+ * @param path
+ * path
+ * @param requestBody
+ * body
+ * @param mediaType
+ * media type
+ * @param headers
+ * headers
+ * @return response
+ */
+ private Response post(final URI path, final String requestBody,
+ final MediaType mediaType, final MultivaluedMap headers) {
+
+ final Client client = getClientWithoutCredentials();
+
+ final WebTarget resource = client.target(path);
+
+ final Invocation.Builder invocationBuilder = resource.request(mediaType);
+
+ final Entity reqBody = Entity.entity(requestBody, mediaType);
+
+ if (headers != null) {
+ invocationBuilder.headers(headers);
+ }
+ final Response res = invocationBuilder.post(reqBody, Response.class);
+
+ return res;
+ }
+
+ /**
+ * Issue a get request to the given uri.
+ *
+ * @param path
+ * uri
+ * @param headers
+ * headers
+ * @return response
+ */
+ public Response get(final URI path,
+ final MultivaluedMap headers) {
+
+ final Client client = getClientWithoutCredentials();
+
+ final WebTarget resource = client.target(path);
+
+ final Invocation.Builder invocationBuilder = resource.request();
+
+ if (headers != null) {
+ invocationBuilder.headers(headers);
+ }
+ final Response res = invocationBuilder.get();
+
+ return res;
+ }
+
+ /**
+ * Get a REST Client with no BA credentials.
+ *
+ * @return the REST Client.
+ */
+ private Client getClientWithoutCredentials() {
+ return RestClientBuilderUtility.newDefaultClient();
+ }
+
+ /**
+ * Retrieve the redirect URI from the request.
+ *
+ * @param req
+ * request
+ * @return redirect URI
+ * @throws RequestException
+ */
+ private String getRedirectUri(final HttpServletRequest req)
+ throws RequestException {
+ String uri = req.getParameter("redirectUri");
+
+ if (uri == null) {
+ uri = req.getParameter("redirect_uri");
+ }
+
+ validateRedirectUri(uri);
+
+ return uri;
+ }
+
+ /**
+ * Check the given redirect uri to ensure it meets validity criteria, eg.
+ * security.
+ *
+ * @param uri
+ * uri
+ * @throws RequestException
+ */
+ private void validateRedirectUri(final String uri) throws RequestException {
+ if (uri == null) {
+ return;
+ }
+
+ final String allUris = getProperties()
+ .getString(PROP_REDIRECT_URI_WHITELIST);
+ for (final String validUri : allUris.split(",")) {
+ if (uri.startsWith(validUri)) {
+ return;
+ }
+ }
+
+ throw new RequestException("Invalid redirect uri: " + uri);
+ }
+
+ /**
+ * Check the given redirect uri to ensure it meets validity criteria, eg.
+ * security.
+ *
+ * @param uri
+ * uri
+ * @throws RequestException
+ */
+ private void validateWasReqUrl(final String uri) throws RequestException {
+ if (uri == null) {
+ return;
+ }
+
+ final String allUris = getProperties()
+ .getString(PROP_REDIRECT_URI_WHITELIST);
+ for (final String validUri : allUris.split(",")) {
+ if (uri.startsWith(validUri)) {
+ return;
+ }
+ }
+
+ throw new RequestException("Invalid was req url: " + uri);
+ }
+
+ /**
+ * Retrieve the client secret for a given client.
+ *
+ * @param clientId
+ * client ID
+ * @return secret
+ * @throws RequestException
+ */
+ private String getClientSecret(final String clientId)
+ throws RequestException {
+ final String key = "client.id." + clientId + ".secret";
+ if (getProperties().containsKey(key)) {
+ return getProperties().getString(key);
+ }
+ throw new RequestException("Couldn't get secret for clientId " + clientId);
+ }
+
+ /**
+ * Retrieve an IAM access token.
+ *
+ * @return IAM token
+ * @throws InformationalException
+ * @throws AppException
+ */
+ private JwtToken getIamToken() throws IOException {
+ final URI tokenPath = UriUtils.getIamTokenUri();
+ final Response tokenResponse = new RestUtils().post(tokenPath,
+ "grant_type=" + URLEncoder.encode(APPID_IDENTITY_GRANT_TYPE)
+ + "&apikey=" + getProperties().getString(PROP_APPID_API_KEY),
+ MediaType.APPLICATION_FORM_URLENCODED_TYPE, null,
+ MediaType.APPLICATION_JSON_TYPE);
+
+ final JwtToken token = mapper
+ .readValue(tokenResponse.readEntity(String.class), JwtToken.class);
+ return token;
+
+ }
+
+ /**
+ * Log the authentication result.
+ *
+ * @param tenantId
+ * tenant ID
+ * @param clientId
+ * client ID
+ * @param email
+ * email
+ * @param protectedUrl
+ * url
+ * @param authResult
+ * result
+ * @param authMsg
+ * message
+ */
+ private void logAuthResult(final String tenantId, final String clientId,
+ final String email, final String protectedUrl, final String authResult,
+ final String authMsg) {
+ final StringBuffer sb = new StringBuffer();
+ sb.append(AUTH_RESULT_MSG_ID);
+ sb.append(AUTH_RESULT_SEPARATOR).append(tenantId);
+ sb.append(AUTH_RESULT_SEPARATOR).append(clientId);
+ sb.append(AUTH_RESULT_SEPARATOR).append(email);
+ sb.append(AUTH_RESULT_SEPARATOR);
+ if (protectedUrl != null) {
+ sb.append(protectedUrl);
+ }
+ sb.append(AUTH_RESULT_SEPARATOR).append(authResult);
+ sb.append(AUTH_RESULT_SEPARATOR);
+ if (authMsg != null) {
+ sb.append(authMsg);
+ }
+ log.info(sb.toString());
+ }
+
+ /**
+ * Retrieve resource bundle properties.
+ *
+ * @return properties
+ */
+ private static ResourceBundle getProperties() {
+ return ResourceBundle.getBundle(PROPERTIES_FILE_NAME);
+ }
+}
diff --git a/openidauth/OpenidAuth-war/src/main/java/com/ibm/openid/authentication/BaseServlet.java b/openidauth/OpenidAuth-war/src/main/java/com/ibm/openid/authentication/BaseServlet.java
new file mode 100644
index 0000000..fe9b05e
--- /dev/null
+++ b/openidauth/OpenidAuth-war/src/main/java/com/ibm/openid/authentication/BaseServlet.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2018 IBM Corp.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.ibm.openid.authentication;
+
+import java.io.IOException;
+import javax.servlet.DispatcherType;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * A base servlet that retrieves common parameters and redirects to the relevant
+ * subservlet.
+ */
+public class BaseServlet extends HttpServlet {
+
+ @Override
+ public void service(final ServletRequest req, final ServletResponse res)
+ throws ServletException, IOException {
+
+ if (req instanceof HttpServletRequest
+ && !req.getDispatcherType().equals(DispatcherType.FORWARD)) {
+ final HttpServletRequest hsr = (HttpServletRequest) req;
+ final String targetRequest = getTarget(hsr);
+ if (targetRequest != null) {
+ final String tenantId = getTenantId(hsr);
+ if (tenantId == null) {
+ ((HttpServletResponse) res).sendError(400, "TenantId is required");
+ } else {
+ req.setAttribute("tenantId", tenantId);
+ req.getRequestDispatcher(targetRequest).forward(req, res);
+ }
+ return;
+ }
+ }
+
+ super.service(req, res);
+ }
+
+ /**
+ * Retrieve the intended target from the request URI.
+ *
+ * @param req
+ * request
+ * @return target
+ */
+ private String getTarget(final HttpServletRequest req) {
+ final String pathInfo = req.getPathInfo();
+ return pathInfo.substring(pathInfo.lastIndexOf("/"));
+ }
+
+ /**
+ * Retrieve the tenantId from the request URI.
+ *
+ * @param req
+ * request
+ * @return tenantId
+ */
+ private String getTenantId(final HttpServletRequest req) {
+ final String pathInfo = req.getPathInfo(); // /{value}/test
+ final String[] pathParts = pathInfo.split("/");
+ if (pathParts.length > 1) {
+ return pathParts[1];
+ }
+
+ return null;
+ }
+}
diff --git a/openidauth/OpenidAuth-war/src/main/java/com/ibm/openid/authentication/PasswordServlet.java b/openidauth/OpenidAuth-war/src/main/java/com/ibm/openid/authentication/PasswordServlet.java
new file mode 100644
index 0000000..5926752
--- /dev/null
+++ b/openidauth/OpenidAuth-war/src/main/java/com/ibm/openid/authentication/PasswordServlet.java
@@ -0,0 +1,264 @@
+/*
+ * Copyright 2018 IBM Corp.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.ibm.openid.authentication;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.ibm.openid.authentication.exception.RequestException;
+import com.ibm.openid.authentication.rest.JwtToken;
+import com.ibm.openid.authentication.rest.RestUtils;
+import com.ibm.openid.authentication.rest.UriUtils;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URLEncoder;
+import java.util.ResourceBundle;
+import javax.servlet.ServletException;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedHashMap;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A simple servlet that performs facilitates password reset requests with an
+ * OAuth2 IdP such as AppID.
+ */
+public class PasswordServlet extends HttpServlet {
+ private static final Logger log = LoggerFactory
+ .getLogger(PasswordServlet.class);
+ private static final String PROPERTIES_FILE_NAME = "com.ibm.openid.authentication.auth";
+
+ private static final String PROP_APPID_API_KEY = "appid.api.key";
+ private static final String PROP_ENVIRONMENT = "environment";
+ private static final String PROP_REDIRECT_URI_WHITELIST = "security.redirect.uri.whitelist";
+
+ private static final String APPID_IDENTITY_GRANT_TYPE = "urn:ibm:params:oauth:grant-type:apikey";
+
+ private static final String WAS_STATE_COOKIE_PREFIX = "WASOidcState";
+
+ protected static final ObjectMapper mapper = new ObjectMapper();
+
+ private static final String LOGIN_JSP = "/login.jsp";
+
+ @Override
+ protected void doGet(final HttpServletRequest req,
+ final HttpServletResponse resp) throws ServletException, IOException {
+
+ this.forwardToLogin(req, resp, getTenantId(req),
+ req.getParameter("client_id"), getRedirectUri(req),
+ req.getParameter("state"), null);
+ }
+
+ @Override
+ protected void doPost(final HttpServletRequest req,
+ final HttpServletResponse resp) throws ServletException, IOException {
+ doPasswordReset(req, resp);
+ }
+
+ /**
+ * Perform a password reset request.
+ *
+ * @param req
+ * request
+ * @param resp
+ * response
+ * @throws ServletException
+ * @throws IOException
+ */
+ private void doPasswordReset(final HttpServletRequest req,
+ final HttpServletResponse resp) throws ServletException, IOException {
+ final String email = req.getParameter("email");
+ final String clientId = req.getParameter("clientId");
+ final String tenantId = getTenantId(req);
+ final String state = req.getParameter("state");
+ final String redirectUri = getRedirectUri(req);
+
+ requestPasswordReset(tenantId, email);
+
+ forwardToLogin(req, resp, tenantId, clientId, redirectUri, state,
+ "A password reset e-mail has been sent to you.");
+ }
+
+ /**
+ * Forward this request and common parameters to the login page
+ *
+ * @param req
+ * request
+ * @param resp
+ * response
+ * @param tenantId
+ * tenant ID
+ * @param clientId
+ * client ID
+ * @param redirectUri
+ * redirect URI
+ * @param state
+ * application state
+ * @param loginMessage
+ * message for the login page
+ * @throws ServletException
+ * @throws IOException
+ */
+ private void forwardToLogin(final HttpServletRequest req,
+ final HttpServletResponse resp, final String tenantId,
+ final String clientId, final String redirectUri, final String state,
+ final String loginMessage) throws ServletException, IOException {
+ req.setAttribute("loginMessage", loginMessage);
+ req.setAttribute("tenantId", tenantId);
+ req.setAttribute("clientId", clientId);
+ req.setAttribute("redirectUri", redirectUri);
+ req.setAttribute("state", state);
+ req.setAttribute("environment",
+ getProperties().getString(PROP_ENVIRONMENT));
+ final Cookie stateCookie = getStateCookie(req);
+ if (stateCookie != null) {
+ req.setAttribute("stateCookie", stateCookie.getName());
+ }
+ req.getServletContext().getRequestDispatcher(LOGIN_JSP).forward(req, resp);
+ }
+
+ /**
+ * Check for the presence of the WAS OIDC state cookie, by prefix.
+ *
+ * @param req
+ * request
+ * @return cookie if the cookie is present
+ */
+ private Cookie getStateCookie(final HttpServletRequest req) {
+ if (req.getCookies() == null) {
+ return null;
+ }
+
+ for (final Cookie cookie : req.getCookies()) {
+ if (cookie.getName().startsWith(WAS_STATE_COOKIE_PREFIX)) {
+ return cookie;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Retrieve the tenantId from the request path.
+ *
+ * @param req
+ * request
+ * @return tenantId
+ */
+ private String getTenantId(final HttpServletRequest req) {
+ return req.getAttribute("tenantId").toString();
+ }
+
+ /**
+ * Retrieve the redirect URI from the request.
+ *
+ * @param req
+ * request
+ * @return redirect URI
+ * @throws RequestException
+ */
+ private String getRedirectUri(final HttpServletRequest req)
+ throws RequestException {
+ String uri = req.getParameter("redirectUri");
+
+ if (uri == null) {
+ uri = req.getParameter("redirect_uri");
+ }
+
+ validateRedirectUri(uri);
+
+ return uri;
+ }
+
+ /**
+ * Request a password reset for the given email.
+ *
+ * @param email
+ * email
+ * @throws IOException
+ *
+ * @throws InformationalException
+ * @throws AppException
+ */
+ private void requestPasswordReset(final String tenantId, final String email)
+ throws IOException {
+ final MultivaluedMap headers = new MultivaluedHashMap();
+ headers.putSingle("Authorization",
+ "Bearer " + getIamToken().getAccessToken());
+ final Response resetResponse = new RestUtils().post(
+ UriUtils.getPasswordResetURI(tenantId),
+ "email=" + URLEncoder.encode(email),
+ MediaType.APPLICATION_FORM_URLENCODED_TYPE, headers, null);
+ log.info("Attempted email '" + email + "' password reset, response: "
+ + resetResponse.getStatus() + " "
+ + resetResponse.readEntity(String.class));
+
+ }
+
+ /**
+ * Retrieve an IAM access token.
+ *
+ * @return IAM token
+ * @throws InformationalException
+ * @throws AppException
+ */
+ private JwtToken getIamToken() throws IOException {
+ final URI tokenPath = UriUtils.getIamTokenUri();
+ final Response tokenResponse = new RestUtils().post(tokenPath,
+ "grant_type=" + URLEncoder.encode(APPID_IDENTITY_GRANT_TYPE)
+ + "&apikey=" + getProperties().getString(PROP_APPID_API_KEY),
+ MediaType.APPLICATION_FORM_URLENCODED_TYPE, null,
+ MediaType.APPLICATION_JSON_TYPE);
+
+ final JwtToken token = mapper
+ .readValue(tokenResponse.readEntity(String.class), JwtToken.class);
+ return token;
+
+ }
+
+ /**
+ * Check the given redirect uri to ensure it meets validity criteria, eg.
+ * security.
+ *
+ * @param uri
+ * uri
+ * @throws RequestException
+ */
+ private void validateRedirectUri(final String uri) throws RequestException {
+ if (uri == null) {
+ return;
+ }
+
+ final String allUris = getProperties()
+ .getString(PROP_REDIRECT_URI_WHITELIST);
+ for (final String validUri : allUris.split(",")) {
+ if (uri.startsWith(validUri)) {
+ return;
+ }
+ }
+
+ throw new RequestException("Invalid redirect uri: " + uri);
+ }
+
+ /**
+ * Retrieve resource bundle properties.
+ *
+ * @return properties
+ */
+ private static ResourceBundle getProperties() {
+ return ResourceBundle.getBundle(PROPERTIES_FILE_NAME);
+ }
+}
diff --git a/openidauth/OpenidAuth-war/src/main/java/com/ibm/openid/authentication/TokenServlet.java b/openidauth/OpenidAuth-war/src/main/java/com/ibm/openid/authentication/TokenServlet.java
new file mode 100644
index 0000000..0ea9feb
--- /dev/null
+++ b/openidauth/OpenidAuth-war/src/main/java/com/ibm/openid/authentication/TokenServlet.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright 2018 IBM Corp.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.ibm.openid.authentication;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.ibm.openid.authentication.exception.ValidationException;
+import com.ibm.openid.authentication.rest.JwtToken;
+import com.ibm.openid.authentication.rest.RestUtils;
+import com.ibm.openid.cache.TokenCache;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.Base64;
+import java.util.ResourceBundle;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A simple servlet that implements the /token endpoint of an OAuth2 IdP.
+ *
+ * Currently only supports the authorization_code grant type.
+ */
+public class TokenServlet extends HttpServlet {
+ private static final Logger log = LoggerFactory.getLogger(TokenServlet.class);
+ private static final String PROPERTIES_FILE_NAME = "com.ibm.openid.authentication.auth";
+
+ @Override
+ protected void doPost(final HttpServletRequest req,
+ final HttpServletResponse resp) throws ServletException, IOException {
+ resp.setContentType("application/json");
+
+ log.debug(
+ "TokenServlet request received for uuid " + req.getParameter("code"));
+
+ try {
+ final String authHeaderClientId = getAuthHeaderClient(req, resp);
+
+ final JwtToken jwtToken = getToken(req, resp, authHeaderClientId);
+
+ final String json = RestUtils.generateJSONString(jwtToken);
+
+ final PrintWriter out = resp.getWriter();
+ out.print(json); // NOSONAR
+ out.flush();
+
+ log.debug("TokenServlet response: " + json);
+
+ } catch (final ValidationException e) { // NOSONAR
+ this.sendError(resp, e.getMessage(), e.getDescription(),
+ e.getStatusCode());
+ }
+ }
+
+ private JwtToken getToken(final HttpServletRequest req,
+ final HttpServletResponse resp, final String authHeaderClientId)
+ throws ValidationException {
+ final String grantType = req.getParameter("grant_type");
+ if (!grantType.equals("authorization_code")) {
+ throw new ValidationException("unsupported_grant_type",
+ "unrecognized grant_type");
+ }
+
+ final String uuid = req.getParameter("code");
+ final JwtToken jwtToken = new TokenCache().get(uuid);
+ if (jwtToken == null) {
+ throw new ValidationException("invalid_grant", "grant code not found");
+ }
+
+ if (!jwtToken.getTenantId().equals(req.getAttribute("tenantId"))
+ || !jwtToken.getClientId().equals(authHeaderClientId)) {
+ throw new ValidationException("invalid_client",
+ "unknown client_id or tenantId");
+ }
+
+ return jwtToken;
+ }
+
+ /**
+ * Returns the clientId in the authorization headers.
+ *
+ * @param req
+ * request
+ * @param resp
+ * response
+ * @return clientId
+ * @throws ValidationException
+ * if the clientId could not be extracted or validated
+ * @throws IOException
+ */
+ private String getAuthHeaderClient(final HttpServletRequest req,
+ final HttpServletResponse resp) throws ValidationException, IOException {
+ try {
+ final String authHeader = req.getHeader("Authorization");
+ if (authHeader == null) {
+ throw new ValidationException(
+ "please provide clientId as username and secret as password",
+ "missing credentials in request");
+ }
+ final String[] values = authHeader.split(" ");
+ final String type = values[0];
+ final String value = values[1];
+
+ if (!type.equals("Basic")) {
+ throw new ValidationException(
+ "please provide clientId as username and secret as password",
+ "missing credentials in request");
+ }
+
+ final String[] decoded = new String(Base64.getDecoder().decode(value))
+ .split(":");
+ final String clientId = decoded[0];
+ final String secret = decoded[1];
+ final String knownSecret = getClientSecret(clientId);
+ if (knownSecret == null) {
+ throw new ValidationException("invalid_client", "unknown client_id");
+ }
+
+ if (!knownSecret.equals(secret)) {
+ throw new ValidationException("invalid_client", "Invalid Credentials",
+ 401);
+ }
+
+ // client is valid, secret is valid
+ return clientId;
+
+ } catch (final ArrayIndexOutOfBoundsException e) {
+ throw new ValidationException(
+ "please provide clientId as username and secret as password",
+ "missing credentials in request", e);
+ }
+ }
+
+ /**
+ * Retrieve the client secret for a given client.
+ *
+ * @param clientId
+ * client ID
+ * @return secret
+ * @throws IOException
+ */
+ private String getClientSecret(final String clientId) throws IOException {
+ final String key = "client.id." + clientId + ".secret";
+ if (getProperties().containsKey(key)) {
+ return getProperties().getString(key);
+ }
+ throw new IOException("Couldn't get secret for clientId " + clientId);
+ }
+
+ /**
+ * Send a bad request error with the given message.
+ *
+ * @param error
+ * name
+ * @param description
+ * description
+ * @param statusCode
+ * status code
+ * @throws IOException
+ * @throws JsonProcessingException
+ */
+ private void sendError(final HttpServletResponse resp, final String error,
+ final String description, final Integer statusCode)
+ throws JsonProcessingException, IOException {
+ final JwtToken jwtToken = new JwtToken();
+ jwtToken.setError(error);
+ jwtToken.setErrorDescription(description);
+ final int code = statusCode != null ? statusCode : 400;
+ resp.setStatus(code);
+
+ final PrintWriter out = resp.getWriter();
+ final String json = RestUtils.generateJSONString(jwtToken);
+ out.print(json); // NOSONAR
+ out.flush();
+
+ log.info("TokenServlet responded with error: " + json);
+
+ return;
+ }
+
+ /**
+ * Retrieve resource bundle properties.
+ *
+ * @return properties
+ */
+ private static ResourceBundle getProperties() {
+ return ResourceBundle.getBundle(PROPERTIES_FILE_NAME);
+ }
+}
diff --git a/openidauth/OpenidAuth-war/src/main/java/com/ibm/openid/authentication/exception/RequestException.java b/openidauth/OpenidAuth-war/src/main/java/com/ibm/openid/authentication/exception/RequestException.java
new file mode 100644
index 0000000..1212100
--- /dev/null
+++ b/openidauth/OpenidAuth-war/src/main/java/com/ibm/openid/authentication/exception/RequestException.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2018 IBM Corp.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.ibm.openid.authentication.exception;
+
+import javax.servlet.ServletException;
+
+public class RequestException extends ServletException {
+
+ public RequestException(final String message) {
+ super(message);
+ }
+}
diff --git a/openidauth/OpenidAuth-war/src/main/java/com/ibm/openid/authentication/exception/ValidationException.java b/openidauth/OpenidAuth-war/src/main/java/com/ibm/openid/authentication/exception/ValidationException.java
new file mode 100644
index 0000000..5ba807e
--- /dev/null
+++ b/openidauth/OpenidAuth-war/src/main/java/com/ibm/openid/authentication/exception/ValidationException.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2018 IBM Corp.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.ibm.openid.authentication.exception;
+
+public class ValidationException extends Exception {
+ private Integer statusCode;
+ private final String description;
+
+ public ValidationException(final String message, final String description,
+ final Throwable t) {
+ super(message, t);
+ this.description = description;
+ }
+
+ public ValidationException(final String message, final String description,
+ final Integer statusCode) {
+ super(message);
+ this.description = description;
+ this.statusCode = statusCode;
+ }
+
+ public ValidationException(final String message, final String description) {
+ super(message);
+ this.description = description;
+ }
+
+ public Integer getStatusCode() {
+ return this.statusCode;
+ }
+
+ public String getDescription() {
+ return this.description;
+ }
+}
diff --git a/openidauth/OpenidAuth-war/src/main/java/com/ibm/openid/authentication/rest/Attributes.java b/openidauth/OpenidAuth-war/src/main/java/com/ibm/openid/authentication/rest/Attributes.java
new file mode 100644
index 0000000..8b72e4f
--- /dev/null
+++ b/openidauth/OpenidAuth-war/src/main/java/com/ibm/openid/authentication/rest/Attributes.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2018 IBM Corp.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.ibm.openid.authentication.rest;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.Map;
+
+/**
+ * A POJO representation of a map of user profile attributes, for AppID user
+ * creation.
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class Attributes {
+ private Map attributes;
+
+ /**
+ * Get the attributes.
+ *
+ * @return attributes
+ */
+ @JsonProperty("attributes")
+ public Map getAttributes() {
+ return attributes;
+ }
+
+ /**
+ * Set the attributes.
+ *
+ * @param attributes
+ * attributes
+ */
+ public void setAttributes(final Map attributes) {
+ this.attributes = attributes;
+ }
+}
diff --git a/openidauth/OpenidAuth-war/src/main/java/com/ibm/openid/authentication/rest/JwtToken.java b/openidauth/OpenidAuth-war/src/main/java/com/ibm/openid/authentication/rest/JwtToken.java
new file mode 100644
index 0000000..5be062f
--- /dev/null
+++ b/openidauth/OpenidAuth-war/src/main/java/com/ibm/openid/authentication/rest/JwtToken.java
@@ -0,0 +1,260 @@
+/*
+ * Copyright 2018 IBM Corp.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.ibm.openid.authentication.rest;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.io.Serializable;
+
+/**
+ * A POJO representation of a web token received as a Json response.
+ */
+
+@SuppressWarnings("all")
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class JwtToken implements Serializable {
+ private String accessToken;
+ private String refreshToken;
+ private String uaaToken;
+ private String uaaRefreshToken;
+ private String tokenType;
+ private Long expiresIn;
+ private Long expiration;
+ private String idToken;
+ private String error;
+ private String errorDescription;
+ private String tenantId;
+ private String clientId;
+
+ /**
+ * Empty constructor.
+ */
+ public JwtToken() {
+ // do nothing.
+ }
+
+ /**
+ * Retrieve the error.
+ *
+ * @return error
+ */
+ @JsonProperty("error")
+ public String getError() {
+ return error;
+ }
+
+ /**
+ * Set the error.
+ *
+ * @param error
+ * error
+ */
+ public void setError(final String error) {
+ this.error = error;
+ }
+
+ /**
+ * Retrieve the error description.
+ *
+ * @return error description
+ */
+ @JsonProperty("error_description")
+ public String getErrorDescription() {
+ return errorDescription;
+ }
+
+ /**
+ * Set the error description.
+ *
+ * @param errorDescription
+ * error description
+ */
+ public void setErrorDescription(final String errorDescription) {
+ this.errorDescription = errorDescription;
+ }
+
+ /**
+ * Retrieve the access token.
+ *
+ * @return access token
+ */
+ @JsonProperty("access_token")
+ public String getAccessToken() {
+ return accessToken;
+ }
+
+ /**
+ * Set the access token.
+ *
+ * @param accessToken
+ * access token
+ */
+ public void setAccessToken(final String accessToken) {
+ this.accessToken = accessToken;
+ }
+
+ /**
+ * Retrieve the id token.
+ *
+ * @return id token
+ */
+ @JsonProperty("id_token")
+ public String getIdToken() {
+ return idToken;
+ }
+
+ /**
+ * Set the id token.
+ *
+ * @param idToken
+ * id token
+ */
+ public void setIdToken(final String idToken) {
+ this.idToken = idToken;
+ }
+
+ /**
+ * Get the refresh token.
+ *
+ * @return refresh token
+ */
+ @JsonProperty("refresh_token")
+ public String getRefreshToken() {
+ return refreshToken;
+ }
+
+ /**
+ * Set the refresh token.
+ *
+ * @param refreshToken
+ * refresh token
+ */
+ public void setRefreshToken(final String refreshToken) {
+ this.refreshToken = refreshToken;
+ }
+
+ /**
+ * Get the UAA token.
+ *
+ * @return uaa token
+ */
+ @JsonProperty("uaa_token")
+ public String getUaaToken() {
+ return uaaToken;
+ }
+
+ /**
+ * Set the UAA token.
+ *
+ * @param uaaToken
+ * uaa token
+ */
+ public void setUaaToken(final String uaaToken) {
+ this.uaaToken = uaaToken;
+ }
+
+ /**
+ * Get the UAA refresh token.
+ *
+ * @return UAA refresh token
+ */
+ @JsonProperty("uaa_refresh_token")
+ public String getUaaRefreshToken() {
+ return uaaRefreshToken;
+ }
+
+ /**
+ * Set the UAA refresh token.
+ *
+ * @param uaaRefreshToken
+ * uaa refresh token
+ */
+ public void setUaaRefreshToken(final String uaaRefreshToken) {
+ this.uaaRefreshToken = uaaRefreshToken;
+ }
+
+ /**
+ * Get the token type.
+ *
+ * @return token type
+ */
+ @JsonProperty("token_type")
+ public String getTokenType() {
+ return tokenType;
+ }
+
+ /**
+ * Set the token type.
+ *
+ * @param tokenType
+ * token type
+ */
+ public void setTokenType(final String tokenType) {
+ this.tokenType = tokenType;
+ }
+
+ /**
+ * Get the expiry time.
+ *
+ * @return expiry time.
+ */
+ @JsonProperty("expires_in")
+ public Long getExpiresIn() {
+ return expiresIn;
+ }
+
+ /**
+ * Set the expiry time.
+ *
+ * @param expiresIn
+ * expiry time
+ */
+ public void setExpiresIn(final Long expiresIn) {
+ this.expiresIn = expiresIn;
+ }
+
+ /**
+ * Get the expiration.
+ *
+ * @return expiration
+ */
+ public Long getExpiration() {
+ return expiration;
+ }
+
+ /**
+ * Set the expiration.
+ *
+ * @param expiration
+ * expiration
+ */
+ public void setExpiration(final Long expiration) {
+ this.expiration = expiration;
+ }
+
+ public String getTenantId() {
+ return tenantId;
+ }
+
+ public void setTenantId(final String tenantId) {
+ this.tenantId = tenantId;
+ }
+
+ public String getClientId() {
+ return clientId;
+ }
+
+ public void setClientId(final String clientId) {
+ this.clientId = clientId;
+ }
+}
diff --git a/openidauth/OpenidAuth-war/src/main/java/com/ibm/openid/authentication/rest/Profile.java b/openidauth/OpenidAuth-war/src/main/java/com/ibm/openid/authentication/rest/Profile.java
new file mode 100644
index 0000000..add3f30
--- /dev/null
+++ b/openidauth/OpenidAuth-war/src/main/java/com/ibm/openid/authentication/rest/Profile.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2018 IBM Corp.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.ibm.openid.authentication.rest;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+/**
+ * A POJO representation of a user profile object, for AppID user creation.
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class Profile {
+ private String idp;
+ private String profileId;
+
+ /**
+ * Get the profileId.
+ *
+ * @return profileId
+ */
+ public String getProfileId() {
+ return profileId;
+ }
+
+ /**
+ * Set the profileId.
+ *
+ * @param profileId
+ * profileId
+ */
+ public void setProfileId(final String profileId) {
+ this.profileId = profileId;
+ }
+
+ /**
+ * Get the idp.
+ *
+ * @return idp
+ */
+ public String getIdp() {
+ return idp;
+ }
+
+ /**
+ * Set the idp.
+ *
+ * @param idp
+ * idp
+ */
+ public void setIdp(final String idp) {
+ this.idp = idp;
+ }
+
+}
diff --git a/openidauth/OpenidAuth-war/src/main/java/com/ibm/openid/authentication/rest/Profiles.java b/openidauth/OpenidAuth-war/src/main/java/com/ibm/openid/authentication/rest/Profiles.java
new file mode 100644
index 0000000..a7547d4
--- /dev/null
+++ b/openidauth/OpenidAuth-war/src/main/java/com/ibm/openid/authentication/rest/Profiles.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2018 IBM Corp.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.ibm.openid.authentication.rest;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.List;
+
+/**
+ * A POJO representation of a list of user object, for AppID user retrieval.
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class Profiles {
+ private List profiles;
+
+ /**
+ * Get the profiles.
+ *
+ * @return profiles
+ */
+ @JsonProperty("profiles")
+ public List getProfiles() {
+ return profiles;
+ }
+
+ /**
+ * Set the profiles.
+ *
+ * @param profiles
+ * profiles
+ */
+ public void setResources(final List profiles) {
+ this.profiles = profiles;
+ }
+}
diff --git a/openidauth/OpenidAuth-war/src/main/java/com/ibm/openid/authentication/rest/RestClientBuilderUtility.java b/openidauth/OpenidAuth-war/src/main/java/com/ibm/openid/authentication/rest/RestClientBuilderUtility.java
new file mode 100644
index 0000000..d30513a
--- /dev/null
+++ b/openidauth/OpenidAuth-war/src/main/java/com/ibm/openid/authentication/rest/RestClientBuilderUtility.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright 2018 IBM Corp.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.ibm.openid.authentication.rest;
+
+import java.util.function.Consumer;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLSession;
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import org.glassfish.jersey.client.ClientConfig;
+
+/**
+ * A utility class to generate a Jersey rest client.
+ */
+public final class RestClientBuilderUtility {
+
+ /**
+ * Default constructor.
+ */
+ private RestClientBuilderUtility() {
+ // No operation
+ }
+
+ /**
+ * Creates a client with Application Server aware SSL settings.
+ *
+ * @return The JAX RS client.
+ */
+ public static Client newDefaultClient() {
+ return newClient(newDefaultClientConfig());
+ }
+
+ /**
+ * Create a client config with the required default. In particular, this sets
+ * up the SSL settings required in Liberty.
+ *
+ * @return The client configuration.
+ */
+ public static ClientConfig newDefaultClientConfig() {
+ return ensureSSLConfig(new ClientConfig());
+ }
+
+ /**
+ * Creates a client with Application Server aware SSL settings. This allows
+ * the API user to customize the {@link CLientBuilder} settings used for the
+ * client.
+ *
+ * Usage:
+ *
+ *
+ * final Client client = RestClientBuilderUtility.newDefaultClient(builder ->
+ * // your customizations here.
+ * );
+ *
+ *
+ * @param clientConfig
+ * The client config.
+ * @return The configured client.
+ */
+ public static Client newClient(final ClientConfig clientConfig) {
+ return newClient(NoopConfigurationCustomizer.kInstance, clientConfig);
+ }
+
+ /**
+ * Creates a client with Application Server aware SSL settings. This allows
+ * the API user to customize the {@link CLientBuilder} settings used for the
+ * client.
+ *
+ * You should NEVER use this unless you need to modify SSL settings for some
+ * reason.
+ *
+ * TODO: left private until I find use case. If you have a valid use case,
+ * make this method public.
+ *
+ * Usage:
+ *
+ *
+ * final ClientConfig clientConfig = RestClientBuilderUtility.newClientConfig();
+ * clientConfig.propert(.., ..);
+ * final Client client = RestClientBuilderUtility.newDefaultClient(builder -> {
+ * // your customizations here.
+ * }, clientConfig);
+ *
+ *
+ * @param builderConfigurer
+ * The builder configurer.
+ * @param clientConfig
+ * The client config.
+ * @return The configured client.
+ */
+ private static Client newClient(
+ final Consumer builderConfigurer,
+ final ClientConfig clientConfig) {
+ final ClientBuilder clientBulder = newClientBuilder();
+
+ // TODO - can not think of a time we would ever not want this. For now, I
+ // will not allow
+ // developers to shoot themselves in the foot.
+
+ // clientBulder.withConfig(ensureSSLConfig(clientConfig));
+ builderConfigurer.accept(clientBulder);
+ return clientBulder.build();
+ }
+
+ /**
+ * Ensures that the client configuration contains the Liberty SSL
+ * configuration.
+ *
+ * @param clientConfig
+ * The client configuration.
+ * @return The corrected client configuration.
+ */
+ private static ClientConfig ensureSSLConfig(final ClientConfig clientConfig) {
+
+ return clientConfig;
+ }
+
+ /**
+ * Corrects the JAXRS client to use the appropriate SSL configuration settings
+ * in Liberty.
+ *
+ * @return The client builder.
+ */
+ private static ClientBuilder newClientBuilder() {
+ return ClientBuilder.newBuilder()
+ .hostnameVerifier(TrustAllHostNameVerifier.kInstance);
+ }
+
+ /**
+ * Implement host name verification that skips host name verification. This is
+ * required in production due to the lack of DNS services.
+ */
+ private static final class TrustAllHostNameVerifier
+ implements HostnameVerifier {
+
+ /**
+ * Instance.
+ */
+ public static final TrustAllHostNameVerifier kInstance = new TrustAllHostNameVerifier();
+
+ /**
+ * Constructor.
+ */
+ private TrustAllHostNameVerifier() {
+ // no body
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean verify(final String hostname, final SSLSession session) {
+ return true;
+ }
+
+ }
+
+ /**
+ * NoOP configuration customizaer.
+ */
+ private static final class NoopConfigurationCustomizer
+ implements Consumer {
+
+ /**
+ * Instance.
+ */
+ public static final NoopConfigurationCustomizer kInstance = new NoopConfigurationCustomizer();
+
+ /**
+ * Constructor.
+ */
+ private NoopConfigurationCustomizer() {
+ // no body
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void accept(final ClientBuilder t) {
+ // Do nothing.
+ }
+
+ }
+}
diff --git a/openidauth/OpenidAuth-war/src/main/java/com/ibm/openid/authentication/rest/RestUtils.java b/openidauth/OpenidAuth-war/src/main/java/com/ibm/openid/authentication/rest/RestUtils.java
new file mode 100644
index 0000000..1304d06
--- /dev/null
+++ b/openidauth/OpenidAuth-war/src/main/java/com/ibm/openid/authentication/rest/RestUtils.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2018 IBM Corp.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.ibm.openid.authentication.rest;
+
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.net.URI;
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.client.Invocation;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+
+public class RestUtils {
+ /**
+ * Issue a post request to the given URI.
+ *
+ * @param path
+ * path
+ * @param requestBody
+ * body
+ * @param mediaType
+ * media type
+ * @param headers
+ * headers
+ * @return response
+ */
+ public Response post(final URI path, final String requestBody,
+ final MediaType mediaType, final MultivaluedMap headers,
+ final MediaType acceptType) {
+
+ final Client client = getClientWithoutCredentials();
+
+ final WebTarget resource = client.target(path);
+
+ final MediaType accept = (acceptType != null ? acceptType : mediaType);
+ final Invocation.Builder invocationBuilder = resource.request(accept);
+
+ final Entity reqBody = Entity.entity(requestBody, mediaType);
+
+ if (headers != null) {
+ invocationBuilder.headers(headers);
+ }
+ final Response res = invocationBuilder.post(reqBody, Response.class);
+
+ return res;
+ }
+
+ /**
+ * Issue a get request to the given uri.
+ *
+ * @param path
+ * uri
+ * @param headers
+ * headers
+ * @return response
+ */
+ public Response get(final URI path,
+ final MultivaluedMap headers) {
+
+ final Client client = getClientWithoutCredentials();
+
+ final WebTarget resource = client.target(path);
+
+ final Invocation.Builder invocationBuilder = resource.request();
+
+ if (headers != null) {
+ invocationBuilder.headers(headers);
+ }
+ final Response res = invocationBuilder.get();
+
+ return res;
+ }
+
+ /**
+ * Generate json string from an object.
+ *
+ * @param parameter
+ * Object to be transferred to json string
+ * @return json string
+ * @throws JsonProcessingException
+ * @throws InformationalException
+ * Generic Exception Signature
+ */
+ @SuppressWarnings("squid:S1166")
+ public static String generateJSONString(final Object parameter)
+ throws JsonProcessingException {
+ final ObjectMapper mapper = new ObjectMapper();
+ // Exclude Null value
+ mapper.setSerializationInclusion(Include.NON_NULL);
+ // Exclude Empty value
+ mapper.setSerializationInclusion(Include.NON_EMPTY);
+ return mapper.writeValueAsString(parameter);
+ }
+
+ /**
+ * Get a REST Client with no BA credentials.
+ *
+ * @return the REST Client.
+ */
+ private Client getClientWithoutCredentials() {
+ return RestClientBuilderUtility.newDefaultClient();
+ }
+
+}
diff --git a/openidauth/OpenidAuth-war/src/main/java/com/ibm/openid/authentication/rest/UriUtils.java b/openidauth/OpenidAuth-war/src/main/java/com/ibm/openid/authentication/rest/UriUtils.java
new file mode 100644
index 0000000..5a6ba04
--- /dev/null
+++ b/openidauth/OpenidAuth-war/src/main/java/com/ibm/openid/authentication/rest/UriUtils.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2018 IBM Corp.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.ibm.openid.authentication.rest;
+
+import java.net.URI;
+import java.net.URLEncoder;
+import java.util.ResourceBundle;
+import javax.ws.rs.core.UriBuilder;
+
+public class UriUtils {
+
+ private static final String PROPERTIES_FILE_NAME = "com.ibm.openid.authentication.auth";
+ private static final String PROP_APPID_AUTH_SERVER = "appid.host";
+ private static final String PROP_APPID_MANAGEMENT_SERVER = "appid.management.host";
+
+ private static final String IAM_TOKEN_URI = "https://iam.bluemix.net/identity/token";
+
+ /**
+ * Retrieve the IAM token uri.
+ *
+ * @return URI
+ */
+ public static URI getIamTokenUri() {
+ return UriBuilder.fromUri(IAM_TOKEN_URI).build();
+ }
+
+ /**
+ * Retrieve the Oauth token URI.
+ *
+ * @param tenantId
+ * tenant ID
+ * @return URI
+ */
+ public static URI getOauthTokenUri(final String tenantId) {
+ final String host = getProperties().getString(PROP_APPID_AUTH_SERVER);
+ return UriBuilder
+ .fromUri("https://" + host + "/oauth/v3/" + tenantId + "/token")
+ .build();
+ }
+
+ /**
+ * Retrieve the authorization page URI.
+ *
+ * @param tenantId
+ * tenant ID
+ * @param clientId
+ * client ID
+ * @param state
+ * state CSRF
+ * @param encodedRedirectUri
+ * @return URI
+ */
+ public static URI getAuthorizationPageUri(final String tenantId,
+ final String clientId, final String state,
+ final String encodedRedirectUri) {
+ final String host = getProperties().getString(PROP_APPID_AUTH_SERVER);
+
+ return UriBuilder.fromUri("https://" + host + "/oauth/v3/" + tenantId
+ + "/authorization?response_type=code&client_id=" + clientId + "&state="
+ + state + "&redirect_uri=" + encodedRedirectUri
+ + "&scope=openid%20profile%20email").build();
+ }
+
+ /**
+ * Retrieve the auth endpoint URI.
+ *
+ * @param tenantId
+ * tenant ID
+ * @return URI
+ */
+ public static URI getAuthUri(final String tenantId) {
+ final String host = getProperties().getString(PROP_APPID_AUTH_SERVER);
+ return UriBuilder.fromUri(
+ "https://" + host + "/oauth/v3/" + tenantId + "/cloud_directory/auth")
+ .build();
+ }
+
+ /**
+ * Retrieve the AppId user password reset URI.
+ *
+ * @return URI
+ */
+ public static URI getPasswordResetURI(final String tenantId) {
+ final String host = getProperties().getString(PROP_APPID_MANAGEMENT_SERVER);
+
+ return UriBuilder.fromUri("https://" + host + "/management/v4/" + tenantId
+ + "/cloud_directory/forgot_password").build();
+ }
+
+ /**
+ * Retrieve the uri for querying user profiles.
+ *
+ * @param userId
+ * userId to search for
+ * @param tenantId
+ * tenant ID
+ * @return uri
+ */
+ public static URI getUserProfileURI(final String userId,
+ final String tenantId) {
+ final String host = getProperties().getString(PROP_APPID_MANAGEMENT_SERVER);
+
+ return UriBuilder.fromUri("https://" + host + "/management/v4/" + tenantId
+ + "/users/" + userId + "/profile").build();
+ }
+
+ /**
+ * Retrieve the uri for querying users.
+ *
+ * @param loginID
+ * login ID to search for
+ * @return uri
+ */
+ public static URI getUserQueryURI(final String loginID,
+ final String tenantId) {
+ final String host = getProperties().getString(PROP_APPID_MANAGEMENT_SERVER);
+ return UriBuilder.fromUri("https://" + host + "/management/v4/" + tenantId
+ + "/users" + "?email=" + URLEncoder.encode(loginID)).build();
+ }
+
+ /**
+ * Retrieve resource bundle properties.
+ *
+ * @return properties
+ */
+ private static ResourceBundle getProperties() {
+ return ResourceBundle.getBundle(PROPERTIES_FILE_NAME);
+ }
+}
diff --git a/openidauth/OpenidAuth-war/src/main/java/com/ibm/openid/authentication/rest/User.java b/openidauth/OpenidAuth-war/src/main/java/com/ibm/openid/authentication/rest/User.java
new file mode 100644
index 0000000..509b69e
--- /dev/null
+++ b/openidauth/OpenidAuth-war/src/main/java/com/ibm/openid/authentication/rest/User.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2018 IBM Corp.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.ibm.openid.authentication.rest;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import java.util.List;
+
+/**
+ * A POJO representation of a user object, for AppID user creation.
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class User {
+ private List emails;
+ private String displayName;
+ private String password;
+ private String id;
+
+ /**
+ * Get the id.
+ *
+ * @return id
+ */
+ public String getId() {
+ return id;
+ }
+
+ /**
+ * Set the id.
+ *
+ * @param id
+ * id
+ */
+ public void setId(final String id) {
+ this.id = id;
+ }
+
+ /**
+ * Get the emails.
+ *
+ * @return emails
+ */
+ public List getEmails() {
+ return emails;
+ }
+
+ /**
+ * Set the emails.
+ *
+ * @param emails
+ * emails
+ */
+ public void setEmails(final List emails) {
+ this.emails = emails;
+ }
+
+ /**
+ * Get the display name.
+ *
+ * @return display name
+ */
+ public String getDisplayName() {
+ return displayName;
+ }
+
+ /**
+ * Set the display name.
+ *
+ * @param displayName
+ * display name
+ */
+ public void setDisplayName(final String displayName) {
+ this.displayName = displayName;
+ }
+
+ /**
+ * Get the password.
+ *
+ * @return password
+ */
+ public String getPassword() {
+ return password;
+ }
+
+ /**
+ * Set the password.
+ *
+ * @param password
+ * password
+ */
+ public void setPassword(final String password) {
+ this.password = password;
+ }
+
+}
diff --git a/openidauth/OpenidAuth-war/src/main/java/com/ibm/openid/authentication/rest/UserEmail.java b/openidauth/OpenidAuth-war/src/main/java/com/ibm/openid/authentication/rest/UserEmail.java
new file mode 100644
index 0000000..d45423c
--- /dev/null
+++ b/openidauth/OpenidAuth-war/src/main/java/com/ibm/openid/authentication/rest/UserEmail.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2018 IBM Corp.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.ibm.openid.authentication.rest;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+/**
+ * A POJO representation of an email object, for AppID user creation.
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class UserEmail {
+ private String value;
+ private Boolean primary;
+
+ /**
+ * Get the value.
+ *
+ * @return value
+ */
+ public String getValue() {
+ return value;
+ }
+
+ /**
+ * Set the value.
+ *
+ * @param value
+ * value
+ */
+ public void setValue(final String value) {
+ this.value = value;
+ }
+
+ /**
+ * Get the primary indicator.
+ *
+ * @return primary
+ */
+ public Boolean getPrimary() {
+ return primary;
+ }
+
+ /**
+ * Set the primary indicator.
+ *
+ * @param primary
+ * primary
+ */
+ public void setPrimary(final Boolean primary) {
+ this.primary = primary;
+ }
+}
diff --git a/openidauth/OpenidAuth-war/src/main/java/com/ibm/openid/authentication/rest/Users.java b/openidauth/OpenidAuth-war/src/main/java/com/ibm/openid/authentication/rest/Users.java
new file mode 100644
index 0000000..99cd33a
--- /dev/null
+++ b/openidauth/OpenidAuth-war/src/main/java/com/ibm/openid/authentication/rest/Users.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2018 IBM Corp.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.ibm.openid.authentication.rest;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.List;
+
+/**
+ * A POJO representation of a list of user object, for AppID user retrieval.
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class Users {
+ private List resources;
+ private List users;
+
+ /**
+ * Get the resources.
+ *
+ * @return resources
+ */
+ @JsonProperty("Resources")
+ public List getResources() {
+ return resources;
+ }
+
+ /**
+ * Set the resources.
+ *
+ * @param resources
+ * resources
+ */
+ public void setResources(final List resources) {
+ this.resources = resources;
+ }
+
+ /**
+ * Get the users.
+ *
+ * @return users
+ */
+ @JsonProperty("users")
+ public List getUsers() {
+ return users;
+ }
+
+ /**
+ * Set the users.
+ *
+ * @param users
+ * users
+ */
+ public void setUsers(final List users) {
+ this.users = users;
+ }
+}
diff --git a/openidauth/OpenidAuth-war/src/main/java/com/ibm/openid/cache/TokenCache.java b/openidauth/OpenidAuth-war/src/main/java/com/ibm/openid/cache/TokenCache.java
new file mode 100644
index 0000000..09b810c
--- /dev/null
+++ b/openidauth/OpenidAuth-war/src/main/java/com/ibm/openid/cache/TokenCache.java
@@ -0,0 +1,33 @@
+package com.ibm.openid.cache;
+
+import com.ibm.openid.authentication.rest.JwtToken;
+import java.net.URL;
+import net.sf.ehcache.Cache;
+import net.sf.ehcache.CacheManager;
+import net.sf.ehcache.Element;
+
+public class TokenCache {
+ private static final String CACHE_NAME = "tokenCache";
+ private static final Cache tokenCache;
+ static {
+ final URL myUrl = TokenCache.class.getResource("ehcache.xml");
+ final CacheManager cacheManager = CacheManager.newInstance(myUrl);
+ tokenCache = cacheManager.getCache(CACHE_NAME);
+ }
+
+ public JwtToken get(final String uuid) {
+ // perform a replace; can only be retrieved once
+ final Element element = tokenCache.get(uuid);
+ if (element != null) {
+ final JwtToken token = (JwtToken) element.getObjectValue();
+ tokenCache.remove(uuid);
+ return token;
+ }
+
+ return null;
+ }
+
+ public void put(final String uuid, final JwtToken token) {
+ tokenCache.put(new Element(uuid, token));
+ }
+}
diff --git a/openidauth/OpenidAuth-war/src/main/resources/com/ibm/openid/authentication/auth.properties b/openidauth/OpenidAuth-war/src/main/resources/com/ibm/openid/authentication/auth.properties
new file mode 100644
index 0000000..8c8d0ef
--- /dev/null
+++ b/openidauth/OpenidAuth-war/src/main/resources/com/ibm/openid/authentication/auth.properties
@@ -0,0 +1,17 @@
+#environment affects display of login page
+environment=dev
+
+#comma-separated list of valid redirect uris
+security.redirect.uri.whitelist=https://hostname:port/oidcclient/,https://hostname:port/protectedapp/
+
+#appid hosts and credentials
+appid.host=appid-oauth.eu-gb.bluemix.net
+appid.management.host=appid-management.eu-gb.bluemix.net
+appid.api.key=
+#client.id.[your_client_id].secret=[your_client_secret]
+client.id.abcd-efgh-ijkl.secret=
+
+#user profile settings
+user.status.attribute=openid-user-status
+user.status.expected=ACTIVE
+
diff --git a/openidauth/OpenidAuth-war/src/main/resources/com/ibm/openid/authentication/text/Login_en.properties b/openidauth/OpenidAuth-war/src/main/resources/com/ibm/openid/authentication/text/Login_en.properties
new file mode 100644
index 0000000..39a533e
--- /dev/null
+++ b/openidauth/OpenidAuth-war/src/main/resources/com/ibm/openid/authentication/text/Login_en.properties
@@ -0,0 +1,10 @@
+#Localization file for login page
+product.name.default=Cloud App
+#product.name.[clientid]=Product Name
+product.name.abcd-efgh-ijkl=
+
+environment.info.dev=Dev Environment
+environment.info.prod=
+
+hipaa.warning.dev=Development instance for trial use only.
+hipaa.warning.prod=
\ No newline at end of file
diff --git a/openidauth/OpenidAuth-war/src/main/resources/com/ibm/openid/cache/ehcache.xml b/openidauth/OpenidAuth-war/src/main/resources/com/ibm/openid/cache/ehcache.xml
new file mode 100644
index 0000000..ed206ec
--- /dev/null
+++ b/openidauth/OpenidAuth-war/src/main/resources/com/ibm/openid/cache/ehcache.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/openidauth/OpenidAuth-war/src/main/webapp/META-INF/MANIFEST.MF b/openidauth/OpenidAuth-war/src/main/webapp/META-INF/MANIFEST.MF
new file mode 100644
index 0000000..3d91273
--- /dev/null
+++ b/openidauth/OpenidAuth-war/src/main/webapp/META-INF/MANIFEST.MF
@@ -0,0 +1,4 @@
+Manifest-Version: 1.0
+Ant-Version: Apache Ant 1.6.5
+Created-By: 1.5.0_06-b05 (Sun Microsystems Inc.)
+
diff --git a/openidauth/OpenidAuth-war/src/main/webapp/WEB-INF/ibm-web-ext.xml b/openidauth/OpenidAuth-war/src/main/webapp/WEB-INF/ibm-web-ext.xml
new file mode 100644
index 0000000..f0cf3ef
--- /dev/null
+++ b/openidauth/OpenidAuth-war/src/main/webapp/WEB-INF/ibm-web-ext.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/openidauth/OpenidAuth-war/src/main/webapp/WEB-INF/web.xml b/openidauth/OpenidAuth-war/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 0000000..8265b80
--- /dev/null
+++ b/openidauth/OpenidAuth-war/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,51 @@
+
+
+
+ OpenidAuth
+
+ An application for custom integration with OAuth2 Identity Providers, such as AppID.
+
+
+
+ Authorization
+ com.ibm.openid.authentication.AuthServlet
+
+
+
+ Authorization
+ /authorization
+
+
+
+ Password
+ com.ibm.openid.authentication.PasswordServlet
+
+
+
+ Password
+ /password
+
+
+
+ Token
+ com.ibm.openid.authentication.TokenServlet
+
+
+
+ Token
+ /token
+
+
+
+ BaseServlet
+ com.ibm.openid.authentication.BaseServlet
+
+
+
+ BaseServlet
+ /*
+
+
diff --git a/openidauth/OpenidAuth-war/src/main/webapp/login.jsp b/openidauth/OpenidAuth-war/src/main/webapp/login.jsp
new file mode 100644
index 0000000..838c054
--- /dev/null
+++ b/openidauth/OpenidAuth-war/src/main/webapp/login.jsp
@@ -0,0 +1,352 @@
+<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
+
+
+
+
+ Login page
Welcome to IBM
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/openidauth/README.md b/openidauth/README.md
new file mode 100644
index 0000000..4d6a463
--- /dev/null
+++ b/openidauth/README.md
@@ -0,0 +1,9 @@
+A simple J2EE webapp containing a custom login page that integrates with openid identity providers such as AppID.
+
+Usage:
+
+`./gradlew eclipse` - generate eclipse artifacts
+
+`./gradlew ear` - build and generate J2EE EAR
+
+For more information, refer to the accompanying article [Custom login page for App ID integration](https://www.ibm.com/blogs/bluemix/2018/06/custom-login-page-app-id-integration/).
diff --git a/openidauth/build.gradle b/openidauth/build.gradle
new file mode 100644
index 0000000..37f083e
--- /dev/null
+++ b/openidauth/build.gradle
@@ -0,0 +1,8 @@
+//apply plugin: 'eclipse'
+
+allprojects {
+ repositories {
+ mavenCentral()
+ }
+}
+
diff --git a/openidauth/gradle.properties b/openidauth/gradle.properties
new file mode 100644
index 0000000..a715a06
--- /dev/null
+++ b/openidauth/gradle.properties
@@ -0,0 +1,2 @@
+# Version management
+version=1.0.0.SNAPSHOT
diff --git a/openidauth/gradle/wrapper/gradle-wrapper.jar b/openidauth/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..3c7abdf
Binary files /dev/null and b/openidauth/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/openidauth/gradle/wrapper/gradle-wrapper.properties b/openidauth/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..de8e181
--- /dev/null
+++ b/openidauth/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Thu Oct 08 17:08:03 CEST 2015
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=http\://services.gradle.org/distributions/gradle-2.7-bin.zip
diff --git a/openidauth/gradlew b/openidauth/gradlew
new file mode 100755
index 0000000..91a7e26
--- /dev/null
+++ b/openidauth/gradlew
@@ -0,0 +1,164 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+esac
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched.
+if $cygwin ; then
+ [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+fi
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >&-
+APP_HOME="`pwd -P`"
+cd "$SAVED" >&-
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/openidauth/gradlew.bat b/openidauth/gradlew.bat
new file mode 100644
index 0000000..8a0b282
--- /dev/null
+++ b/openidauth/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/openidauth/settings.gradle b/openidauth/settings.gradle
new file mode 100644
index 0000000..fc3aaa3
--- /dev/null
+++ b/openidauth/settings.gradle
@@ -0,0 +1,2 @@
+include ':OpenidAuth-ear'
+include ':OpenidAuth-war'
\ No newline at end of file