Java tutorial
// Copyright (c) Microsoft Corporation. // All rights reserved. // // This code is licensed under the MIT License. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files(the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and / or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions : // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. package com.microsoft.aad.adal; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.UUID; import org.json.JSONException; import android.net.Uri; /** * Instance and Tenant discovery. It takes authorization endpoint and sends * query to known hard coded instances to get tenant discovery endpoint. If * instance is valid, it will return tenant discovery endpoint info. Instance * discovery endpoint does not verify tenant info, so Discovery implementation * sends common as a tenant name. Discovery checks only authorization endpoint. * It does not do tenant verification. Initialize and call from UI thread. */ final class Discovery implements IDiscovery { private static final String TAG = "Discovery"; private static final String API_VERSION_KEY = "api-version"; private static final String API_VERSION_VALUE = "1.0"; private static final String AUTHORIZATION_ENDPOINT_KEY = "authorization_endpoint"; private static final String INSTANCE_DISCOVERY_SUFFIX = "common/discovery/instance"; private static final String AUTHORIZATION_COMMON_ENDPOINT = "/common/oauth2/authorize"; private static final String TENANT_DISCOVERY_ENDPOINT = "tenant_discovery_endpoint"; /** * Sync set of valid hosts to skip query to server if host was verified * before. */ private static final Set<String> sValidHosts = Collections.synchronizedSet(new HashSet<String>()); /** * Discovery query will go to the prod only for now. */ private static final String TRUSTED_QUERY_INSTANCE = "login.windows.net"; private UUID mCorrelationId; /** * interface to use in testing. */ private IWebRequestHandler mWebrequestHandler; public Discovery() { initValidList(); mWebrequestHandler = new WebRequestHandler(); } @Override public boolean isValidAuthority(URL authorizationEndpoint) { // For comparison purposes, convert to lowercase Locale.US // getProtocol returns scheme and it is available if it is absolute url // Authority is in the form of https://Instance/tenant/somepath if (authorizationEndpoint != null && !StringExtensions.IsNullOrBlank(authorizationEndpoint.getHost()) && authorizationEndpoint.getProtocol().equals("https") && StringExtensions.IsNullOrBlank(authorizationEndpoint.getQuery()) && StringExtensions.IsNullOrBlank(authorizationEndpoint.getRef()) && !StringExtensions.IsNullOrBlank(authorizationEndpoint.getPath())) { if (UrlExtensions.isADFSAuthority(authorizationEndpoint)) { Logger.e(TAG, "Instance validation returned error", "", ADALError.DEVELOPER_AUTHORITY_CAN_NOT_BE_VALIDED, new AuthenticationException(ADALError.DISCOVERY_NOT_SUPPORTED)); return false; } else if (sValidHosts.contains(authorizationEndpoint.getHost().toLowerCase(Locale.US))) { // host can be the instance or inside the validated list. // Valid hosts will help to skip validation if validated before // call Callback and skip the look up return true; } else { // Only query from Prod instance for now, not all of the // instances in the list return queryInstance(authorizationEndpoint); } } return false; } /** * add this host as valid to skip another query to server. * * @param validhost */ private void addValidHostToList(URL validhost) { String validHost = validhost.getHost(); if (!StringExtensions.IsNullOrBlank(validHost)) { // for comparisons it uses Locale.US, so it needs to be same // here sValidHosts.add(validHost.toLowerCase(Locale.US)); } } /** * initialize initial valid host list with known instances. */ private void initValidList() { // mValidHosts is a sync set if (sValidHosts.size() == 0) { sValidHosts.add("login.windows.net"); // Microsoft Azure Worldwide - Used in validation scenarios where host is not this list sValidHosts.add("login.microsoftonline.com"); // Microsoft Azure Worldwide sValidHosts.add("login.chinacloudapi.cn"); // Microsoft Azure China sValidHosts.add("login.microsoftonline.de"); // Microsoft Azure Germany sValidHosts.add("login-us.microsoftonline.com"); // Microsoft Azure US Government } } private boolean queryInstance(final URL authorizationEndpointUrl) { // It will query prod instance to verify the authority // construct query string for this instance URL queryUrl; boolean result = false; try { queryUrl = buildQueryString(TRUSTED_QUERY_INSTANCE, getAuthorizationCommonEndpoint(authorizationEndpointUrl)); result = sendRequest(queryUrl); } catch (MalformedURLException e) { Logger.e(TAG, "Invalid authority", "", ADALError.DEVELOPER_AUTHORITY_IS_NOT_VALID_URL, e); result = false; } catch (IOException e) { Logger.e(TAG, "Network error", "", ADALError.DEVELOPER_AUTHORITY_CAN_NOT_BE_VALIDED, e); result = false; } catch (JSONException e) { Logger.e(TAG, "Json parsing error", "", ADALError.DEVELOPER_AUTHORITY_CAN_NOT_BE_VALIDED, e); result = false; } if (result) { // it is validated addValidHostToList(authorizationEndpointUrl); } return result; } private boolean sendRequest(final URL queryUrl) throws IOException, JSONException { Logger.v(TAG, "Sending discovery request to:" + queryUrl); Map<String, String> headers = new HashMap<String, String>(); headers.put(WebRequestHandler.HEADER_ACCEPT, WebRequestHandler.HEADER_ACCEPT_JSON); // CorrelationId is used to track the request at the Azure services if (mCorrelationId != null) { headers.put(AuthenticationConstants.AAD.CLIENT_REQUEST_ID, mCorrelationId.toString()); headers.put(AuthenticationConstants.AAD.RETURN_CLIENT_REQUEST_ID, "true"); } HttpWebResponse webResponse = null; String errorCodes = ""; try { ClientMetrics.INSTANCE.beginClientMetricsRecord(queryUrl, mCorrelationId, headers); try { webResponse = mWebrequestHandler.sendGet(queryUrl, headers); ClientMetrics.INSTANCE.setLastError(null); } catch (IOException e) { ClientMetrics.INSTANCE.setLastError(String.valueOf(webResponse.getStatusCode())); throw e; } // parse discovery response to find tenant info final Map<String, String> discoveryResponse = parseResponse(webResponse); if (discoveryResponse.containsKey(AuthenticationConstants.OAuth2.ERROR_CODES)) { errorCodes = discoveryResponse.get(AuthenticationConstants.OAuth2.ERROR_CODES); ClientMetrics.INSTANCE.setLastError(errorCodes); } return (discoveryResponse != null && discoveryResponse.containsKey(TENANT_DISCOVERY_ENDPOINT)); } finally { ClientMetrics.INSTANCE.endClientMetricsRecord(ClientMetricsEndpointType.INSTANCE_DISCOVERY, mCorrelationId); } } /** * get Json output from web response body. If it is well formed response, it * will have tenant discovery endpoint. * * @param webResponse * @return true if tenant discovery endpoint is reported. false otherwise. * @throws JSONException */ private Map<String, String> parseResponse(HttpWebResponse webResponse) throws JSONException { return HashMapExtensions.getJsonResponse(webResponse); } /** * service side does not validate tenant, so it is sending common keyword as * tenant. * * @param authorizationEndpointUrl * @return https://hostname/common */ private String getAuthorizationCommonEndpoint(final URL authorizationEndpointUrl) { return new Uri.Builder().scheme("https").authority(authorizationEndpointUrl.getHost()) .appendPath(AUTHORIZATION_COMMON_ENDPOINT).build().toString(); } /** * It will build query url to check the authorization endpoint. * * @param instance * @param authorizationEndpointUrl * @return * @throws MalformedURLException */ private URL buildQueryString(final String instance, final String authorizationEndpointUrl) throws MalformedURLException { Uri.Builder builder = new Uri.Builder(); builder.scheme("https").authority(instance); // replacing tenant to common since instance validation does not check // tenant name builder.appendEncodedPath(INSTANCE_DISCOVERY_SUFFIX) .appendQueryParameter(API_VERSION_KEY, API_VERSION_VALUE) .appendQueryParameter(AUTHORIZATION_ENDPOINT_KEY, authorizationEndpointUrl); return new URL(builder.build().toString()); } @Override public void setCorrelationId(UUID requestCorrelationId) { mCorrelationId = requestCorrelationId; } }