├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── build.gradle ├── gradle.properties ├── settings.gradle └── src └── main ├── AndroidManifest.xml ├── java └── com │ └── microsoft │ └── services │ └── msa │ ├── AccessTokenRequest.java │ ├── AuthorizationRequest.java │ ├── Config.java │ ├── DefaultObservableOAuthRequest.java │ ├── DeviceType.java │ ├── ErrorMessages.java │ ├── LiveAuthClient.java │ ├── LiveAuthException.java │ ├── LiveAuthListener.java │ ├── LiveConnectSession.java │ ├── LiveConnectUtils.java │ ├── LiveOperationException.java │ ├── LiveStatus.java │ ├── MicrosoftOAuthConfig.java │ ├── OAuth.java │ ├── OAuthConfig.java │ ├── OAuthErrorResponse.java │ ├── OAuthRequestObserver.java │ ├── OAuthResponse.java │ ├── OAuthResponseVisitor.java │ ├── OAuthSuccessfulResponse.java │ ├── ObservableOAuthRequest.java │ ├── OverwriteOption.java │ ├── PreferencesConstants.java │ ├── QueryParameters.java │ ├── RefreshAccessTokenRequest.java │ ├── ScreenSize.java │ ├── TokenRequest.java │ ├── TokenRequestAsync.java │ └── UriBuilder.java └── res └── values └── strings.xml /.gitignore: -------------------------------------------------------------------------------- 1 | .settings/ 2 | .loadpath 3 | .metadata 4 | 5 | *.keystore 6 | *.orig 7 | *.log 8 | *.tmp 9 | *.bak 10 | 11 | # built application files 12 | *.apk 13 | *.ap_ 14 | 15 | # files for the dex VM 16 | *.dex 17 | 18 | # Java class files 19 | *.class 20 | 21 | # Android Studio & Gradle 22 | .idea/ 23 | .gradle/ 24 | local.properties 25 | /*/out 26 | /*/*/build 27 | /*/*/production 28 | *.iws 29 | *.ipr 30 | *.iml 31 | *~ 32 | *.swp 33 | *.pro 34 | 35 | # generated files 36 | bin/ 37 | gen/ 38 | target/ 39 | gen-external-apklibs/ 40 | 41 | # Ignore gradle files 42 | build/ 43 | 44 | # Proguard folder generated by Eclipse 45 | proguard/ 46 | 47 | # Mac 48 | .DS_Store 49 | 50 | ## Gradle 51 | .gradle/ 52 | gradle/ 53 | gradlew 54 | gradlew.bat 55 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: android 2 | android: 3 | components: 4 | - platform-tools 5 | - android-22 6 | - build-tools-22.0.1 7 | 8 | script: 9 | - gradle clean build 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Microsoft Open Technologies, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## MSA Auth for Android 2 | 3 | [ ![Download](https://api.bintray.com/packages/msopentech/Maven/msa-auth-for-android/images/download.svg) ](https://bintray.com/msopentech/Maven/msa-auth-for-android/_latestVersion) 4 | [![Build](https://travis-ci.org/MSOpenTech/msa-auth-for-android.svg?branch=master)](https://travis-ci.org/MSOpenTech/msa-auth-for-android) 5 | 6 | The MSA Auth for Android library is intended to help developers to easily integrate OneDrive and OneNote into their Android apps. 7 | 8 | ## Contributing 9 | You will need to sign a [Contributor License Agreement](https://cla.msopentech.com/) before submitting your pull request. To complete the Contributor License Agreement (CLA), you will need to submit a request via the form and then electronically sign the Contributor License Agreement when you receive the email containing the link to the document. This needs to only be done once for any Microsoft Open Technologies OSS project. 10 | 11 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information, see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 12 | 13 | ## License 14 | Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. Licensed under the MIT License, Version 2.0. 15 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'maven' 3 | 4 | buildscript { 5 | repositories { 6 | jcenter() 7 | } 8 | 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:1.2.3' 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | jcenter() 17 | maven { 18 | url project.nightliesUrl 19 | } 20 | } 21 | } 22 | 23 | if (JavaVersion.current().isJava8Compatible()) { 24 | allprojects { 25 | tasks.withType(Javadoc) { 26 | options.addStringOption('Xdoclint:none', '-quiet') 27 | } 28 | } 29 | } 30 | 31 | android { 32 | compileSdkVersion 22 33 | buildToolsVersion "22.0.1" 34 | 35 | // squelching errors from msa 36 | android { 37 | lintOptions { 38 | abortOnError false 39 | } 40 | } 41 | 42 | defaultConfig { 43 | minSdkVersion 14 44 | targetSdkVersion 22 45 | versionCode 1 46 | versionName "1.0" 47 | } 48 | buildTypes { 49 | release { 50 | minifyEnabled false 51 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 52 | } 53 | } 54 | } 55 | 56 | dependencies { 57 | compile fileTree(dir: 'libs', include: ['*.jar']) 58 | } 59 | 60 | 61 | uploadArchives { 62 | configuration = configurations.archives 63 | repositories.mavenDeployer { 64 | pom { 65 | setGroupId project.mavenGroupId 66 | setArtifactId project.mavenArtifactId 67 | setVersion project.mavenVersion 68 | } 69 | repository (url: project.mavenRepoUrl) { 70 | authentication( 71 | // put these values in local file ~/.gradle/gradle.properties 72 | userName: project.hasProperty("bintrayUsername") ? project.bintrayUsername : "", 73 | password: project.hasProperty("bintrayApikey") ? project.bintrayApikey : "" 74 | ) 75 | } 76 | } 77 | } 78 | 79 | task javadoc(type: Javadoc) { 80 | source = android.sourceSets.main.java.srcDirs 81 | classpath += project.files(android.getBootClasspath()) 82 | classpath += configurations.compile 83 | } 84 | 85 | task javadocJar(type: Jar, dependsOn: javadoc) { 86 | classifier = 'javadoc' 87 | from javadoc.destinationDir 88 | } 89 | 90 | task sourcesJar(type: Jar) { 91 | classifier = 'sources' 92 | from android.sourceSets.main.java.srcDirs 93 | } 94 | 95 | artifacts { 96 | archives javadocJar 97 | archives sourcesJar 98 | } 99 | 100 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # These properties are referenced for Maven repo upload 2 | 3 | mavenRepoUrl = https://api.bintray.com/maven/msopentech/Maven/msa-auth-for-android 4 | mavenGroupId = com.microsoft.services.msa 5 | mavenArtifactId = msa-auth 6 | mavenVersion = 0.8.6 7 | nightliesUrl = http://dl.bintray.com/msopentech/Maven 8 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/main/java/com/microsoft/services/msa/AccessTokenRequest.java: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) 2014 Microsoft Corporation 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // ------------------------------------------------------------------------------ 22 | 23 | package com.microsoft.services.msa; 24 | 25 | import java.util.List; 26 | import java.util.Locale; 27 | 28 | import org.apache.http.NameValuePair; 29 | import org.apache.http.client.HttpClient; 30 | import org.apache.http.message.BasicNameValuePair; 31 | 32 | import android.text.TextUtils; 33 | 34 | /** 35 | * AccessTokenRequest represents a request for an Access Token. 36 | * It subclasses the abstract class TokenRequest, which does most of the work. 37 | * This class adds the proper parameters for the access token request via the 38 | * constructBody() hook. 39 | */ 40 | class AccessTokenRequest extends TokenRequest { 41 | 42 | /** 43 | * REQUIRED. The authorization code received from the 44 | * authorization server. 45 | */ 46 | private final String code; 47 | 48 | /** REQUIRED. Value MUST be set to "authorization_code". */ 49 | private final OAuth.GrantType grantType; 50 | 51 | /** 52 | * Constructs a new AccessTokenRequest, and initializes its member variables 53 | * 54 | * @param client the HttpClient to make HTTP requests on 55 | * @param clientId the client_id of the calling application 56 | * @param code the authorization code received from the AuthorizationRequest 57 | */ 58 | public AccessTokenRequest(final HttpClient client, 59 | final String clientId, 60 | final String code, 61 | final OAuthConfig oAuthConfig) { 62 | super(client, clientId, oAuthConfig); 63 | 64 | if (TextUtils.isEmpty(code)) 65 | throw new AssertionError(); 66 | 67 | this.code = code; 68 | this.grantType = OAuth.GrantType.AUTHORIZATION_CODE; 69 | } 70 | 71 | /** 72 | * Adds the "code", "redirect_uri", and "grant_type" parameters to the body. 73 | * 74 | * @param body the list of NameValuePairs to be placed in the body of the HTTP request 75 | */ 76 | @Override 77 | protected void constructBody(List body) { 78 | body.add(new BasicNameValuePair(OAuth.CODE, this.code)); 79 | body.add(new BasicNameValuePair(OAuth.REDIRECT_URI, mOAuthConfig.getDesktopUri().toString())); 80 | body.add(new BasicNameValuePair(OAuth.GRANT_TYPE, 81 | this.grantType.toString().toLowerCase(Locale.US))); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/com/microsoft/services/msa/AuthorizationRequest.java: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) 2014 Microsoft Corporation 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // ------------------------------------------------------------------------------ 22 | 23 | package com.microsoft.services.msa; 24 | 25 | import android.annotation.SuppressLint; 26 | import android.app.Activity; 27 | import android.app.Dialog; 28 | import android.content.Context; 29 | import android.content.DialogInterface; 30 | import android.content.DialogInterface.OnCancelListener; 31 | import android.content.SharedPreferences; 32 | import android.content.SharedPreferences.Editor; 33 | import android.net.Uri; 34 | import android.os.AsyncTask; 35 | import android.os.Bundle; 36 | import android.text.TextUtils; 37 | import android.view.View; 38 | import android.view.ViewGroup.LayoutParams; 39 | import android.webkit.CookieManager; 40 | import android.webkit.CookieSyncManager; 41 | import android.webkit.WebSettings; 42 | import android.webkit.WebView; 43 | import android.webkit.WebViewClient; 44 | import android.widget.LinearLayout; 45 | 46 | import org.apache.http.client.HttpClient; 47 | 48 | import java.util.Arrays; 49 | import java.util.Comparator; 50 | import java.util.HashMap; 51 | import java.util.HashSet; 52 | import java.util.Locale; 53 | import java.util.Map; 54 | import java.util.Set; 55 | 56 | 57 | /** 58 | * AuthorizationRequest performs an Authorization Request by launching a WebView Dialog that 59 | * displays the login and consent page and then, on a successful login and consent, performs an 60 | * async AccessToken request. 61 | */ 62 | class AuthorizationRequest implements ObservableOAuthRequest, OAuthRequestObserver { 63 | 64 | /** 65 | * OAuthDialog is a Dialog that contains a WebView. The WebView loads the passed in Uri, and 66 | * loads the passed in WebViewClient that allows the WebView to be observed (i.e., when a page 67 | * loads the WebViewClient will be notified). 68 | */ 69 | private class OAuthDialog extends Dialog implements OnCancelListener { 70 | 71 | /** 72 | * AuthorizationWebViewClient is a static (i.e., does not have access to the instance that 73 | * created it) class that checks for when the end_uri is loaded in to the WebView and calls 74 | * the AuthorizationRequest's onEndUri method. 75 | */ 76 | private class AuthorizationWebViewClient extends WebViewClient { 77 | 78 | private final CookieManager cookieManager; 79 | private final Set cookieKeys; 80 | 81 | public AuthorizationWebViewClient() { 82 | // I believe I need to create a syncManager before I can use a cookie manager. 83 | CookieSyncManager.createInstance(getContext()); 84 | this.cookieManager = CookieManager.getInstance(); 85 | this.cookieKeys = new HashSet(); 86 | } 87 | 88 | /** 89 | * Call back used when a page is being started. 90 | * 91 | * This will check to see if the given URL is one of the end_uris/redirect_uris and 92 | * based on the query parameters the method will either return an error, or proceed with 93 | * an AccessTokenRequest. 94 | * 95 | * @param view {@link WebView} that this is attached to. 96 | * @param url of the page being started 97 | */ 98 | @Override 99 | public void onPageFinished(WebView view, String url) { 100 | Uri uri = Uri.parse(url); 101 | 102 | // only clear cookies that are on the logout domain. 103 | if (mOAuthConfig.getLogoutUri().getHost().equals(uri.getHost())) { 104 | this.saveCookiesInMemory(this.cookieManager.getCookie(url)); 105 | } 106 | 107 | Uri endUri = mOAuthConfig.getDesktopUri(); 108 | boolean isEndUri = UriComparator.INSTANCE.compare(uri, endUri) == 0; 109 | if (!isEndUri) { 110 | return; 111 | } 112 | 113 | this.saveCookiesToPreferences(); 114 | 115 | AuthorizationRequest.this.onEndUri(uri); 116 | dismissDialog(); 117 | } 118 | 119 | private void dismissDialog() { 120 | if (isShowing() && activity != null && !activity.isFinishing()) { 121 | OAuthDialog.this.dismiss(); 122 | } 123 | } 124 | 125 | /** 126 | * Callback when the WebView received an Error. 127 | * 128 | * This method will notify the listener about the error and dismiss the WebView dialog. 129 | * 130 | * @param view the WebView that received the error 131 | * @param errorCode the error code corresponding to a WebViewClient.ERROR_* value 132 | * @param description the String containing the description of the error 133 | * @param failingUrl the url that encountered an error 134 | */ 135 | @Override 136 | public void onReceivedError(WebView view, 137 | int errorCode, 138 | String description, 139 | String failingUrl) { 140 | if (errorCode == WebViewClient.ERROR_UNSUPPORTED_SCHEME) { 141 | return; 142 | } 143 | 144 | AuthorizationRequest.this.onError("", description, failingUrl); 145 | dismissDialog(); 146 | } 147 | 148 | private void saveCookiesInMemory(String cookie) { 149 | // Not all URLs will have cookies 150 | if (TextUtils.isEmpty(cookie)) { 151 | return; 152 | } 153 | 154 | String[] pairs = TextUtils.split(cookie, "; "); 155 | for (String pair : pairs) { 156 | int index = pair.indexOf(EQUALS); 157 | String key = pair.substring(0, index); 158 | this.cookieKeys.add(key); 159 | } 160 | } 161 | 162 | private void saveCookiesToPreferences() { 163 | SharedPreferences preferences = 164 | getContext().getSharedPreferences(PreferencesConstants.FILE_NAME, 165 | Context.MODE_PRIVATE); 166 | 167 | // If the application tries to login twice, before calling logout, there could 168 | // be a cookie that was sent on the first login, that was not sent in the second 169 | // login. So, read the cookies in that was saved before, and perform a union 170 | // with the new cookies. 171 | String value = preferences.getString(PreferencesConstants.COOKIES_KEY, ""); 172 | String[] valueSplit = TextUtils.split(value, PreferencesConstants.COOKIE_DELIMITER); 173 | 174 | this.cookieKeys.addAll(Arrays.asList(valueSplit)); 175 | 176 | Editor editor = preferences.edit(); 177 | value = TextUtils.join(PreferencesConstants.COOKIE_DELIMITER, this.cookieKeys); 178 | editor.putString(PreferencesConstants.COOKIES_KEY, value); 179 | editor.commit(); 180 | 181 | // we do not need to hold on to the cookieKeys in memory anymore. 182 | // It could be garbage collected when this object does, but let's clear it now, 183 | // since it will not be used again in the future. 184 | this.cookieKeys.clear(); 185 | } 186 | } 187 | 188 | private WebView webView; 189 | /** Uri to load */ 190 | private final Uri requestUri; 191 | 192 | /** 193 | * Constructs a new OAuthDialog. 194 | * 195 | * @param requestUri to load in the WebView 196 | */ 197 | public OAuthDialog(Uri requestUri) { 198 | super(AuthorizationRequest.this.activity, android.R.style.Theme_Translucent_NoTitleBar); 199 | this.setOwnerActivity(AuthorizationRequest.this.activity); 200 | 201 | if (requestUri == null) throw new AssertionError(); 202 | this.requestUri = requestUri; 203 | } 204 | 205 | /** Called when the user hits the back button on the dialog. */ 206 | @Override 207 | public void onCancel(DialogInterface dialog) { 208 | LiveAuthException exception = new LiveAuthException(ErrorMessages.SIGNIN_CANCEL); 209 | AuthorizationRequest.this.onException(exception); 210 | } 211 | 212 | @SuppressLint("SetJavaScriptEnabled") 213 | @Override 214 | protected void onCreate(Bundle savedInstanceState) { 215 | super.onCreate(savedInstanceState); 216 | 217 | this.setOnCancelListener(this); 218 | 219 | final LinearLayout webViewContainer = new LinearLayout(this.getContext()); 220 | 221 | if (webView == null) { 222 | webView = new WebView(this.getContext()); 223 | webView.setWebViewClient(new AuthorizationWebViewClient()); 224 | final WebSettings webSettings = webView.getSettings(); 225 | webSettings.setJavaScriptEnabled(true); 226 | webView.loadUrl(this.requestUri.toString()); 227 | webView.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, 228 | LayoutParams.MATCH_PARENT)); 229 | webView.setVisibility(View.VISIBLE); 230 | } 231 | webViewContainer.addView(webView); 232 | webViewContainer.setVisibility(View.VISIBLE); 233 | webViewContainer.forceLayout(); 234 | 235 | this.addContentView(webViewContainer, new LayoutParams(LayoutParams.MATCH_PARENT, 236 | LayoutParams.MATCH_PARENT)); 237 | } 238 | } 239 | 240 | /** 241 | * Compares just the scheme, authority, and path. It does not compare the query parameters or 242 | * the fragment. 243 | */ 244 | private enum UriComparator implements Comparator { 245 | INSTANCE; 246 | 247 | @Override 248 | public int compare(Uri lhs, Uri rhs) { 249 | String[] lhsParts = { lhs.getScheme(), lhs.getAuthority(), lhs.getPath() }; 250 | String[] rhsParts = { rhs.getScheme(), rhs.getAuthority(), rhs.getPath() }; 251 | 252 | if (lhsParts.length != rhsParts.length) throw new AssertionError(); 253 | for (int i = 0; i < lhsParts.length; i++) { 254 | if (lhsParts[i] == null && rhsParts[i] == null) { 255 | return 0; 256 | } 257 | 258 | int compare = lhsParts[i].compareTo(rhsParts[i]); 259 | if (compare != 0) { 260 | return compare; 261 | } 262 | } 263 | 264 | return 0; 265 | } 266 | } 267 | 268 | private static final String AMPERSAND = "&"; 269 | private static final String EQUALS = "="; 270 | 271 | /** 272 | * Turns the fragment parameters of the uri into a map. 273 | * 274 | * @param uri to get fragment parameters from 275 | * @return a map containing the fragment parameters 276 | */ 277 | private static Map getFragmentParametersMap(Uri uri) { 278 | String fragment = uri.getFragment(); 279 | String[] keyValuePairs = TextUtils.split(fragment, AMPERSAND); 280 | Map fragementParameters = new HashMap(); 281 | 282 | for (String keyValuePair : keyValuePairs) { 283 | int index = keyValuePair.indexOf(EQUALS); 284 | String key = keyValuePair.substring(0, index); 285 | String value = keyValuePair.substring(index + 1); 286 | fragementParameters.put(key, value); 287 | } 288 | 289 | return fragementParameters; 290 | } 291 | 292 | private final Activity activity; 293 | private final HttpClient client; 294 | private final String clientId; 295 | private final DefaultObservableOAuthRequest observable; 296 | private final String scope; 297 | private final String loginHint; 298 | private final OAuthConfig mOAuthConfig; 299 | 300 | public AuthorizationRequest(Activity activity, 301 | HttpClient client, 302 | String clientId, 303 | String scope, 304 | String loginHint, 305 | final OAuthConfig oAuthConfig) { 306 | if (activity == null) throw new AssertionError(); 307 | if (client == null) throw new AssertionError(); 308 | if (TextUtils.isEmpty(clientId)) throw new AssertionError(); 309 | if (TextUtils.isEmpty(scope)) throw new AssertionError(); 310 | 311 | this.activity = activity; 312 | this.client = client; 313 | this.clientId = clientId; 314 | this.mOAuthConfig = oAuthConfig; 315 | this.observable = new DefaultObservableOAuthRequest(); 316 | this.scope = scope; 317 | this.loginHint = loginHint; 318 | } 319 | 320 | @Override 321 | public void addObserver(OAuthRequestObserver observer) { 322 | this.observable.addObserver(observer); 323 | } 324 | 325 | /** 326 | * Launches the login/consent page inside of a Dialog that contains a WebView and then performs 327 | * a AccessTokenRequest on successful login and consent. This method is async and will call the 328 | * passed in listener when it is completed. 329 | */ 330 | public void execute() { 331 | String displayType = this.getDisplayParameter(); 332 | String responseType = OAuth.ResponseType.CODE.toString().toLowerCase(Locale.US); 333 | String locale = Locale.getDefault().toString(); 334 | final Uri.Builder requestUriBuilder = mOAuthConfig.getAuthorizeUri() 335 | .buildUpon() 336 | .appendQueryParameter(OAuth.CLIENT_ID, this.clientId) 337 | .appendQueryParameter(OAuth.SCOPE, this.scope) 338 | .appendQueryParameter(OAuth.DISPLAY, displayType) 339 | .appendQueryParameter(OAuth.RESPONSE_TYPE, responseType) 340 | .appendQueryParameter(OAuth.REDIRECT_URI, mOAuthConfig.getDesktopUri().toString()); 341 | 342 | if (this.loginHint != null) { 343 | requestUriBuilder.appendQueryParameter(OAuth.LOGIN_HINT, this.loginHint); 344 | requestUriBuilder.appendQueryParameter(OAuth.USER_NAME, this.loginHint); 345 | } 346 | 347 | Uri requestUri = requestUriBuilder.build(); 348 | 349 | OAuthDialog oAuthDialog = new OAuthDialog(requestUri); 350 | oAuthDialog.show(); 351 | } 352 | 353 | @Override 354 | public void onException(LiveAuthException exception) { 355 | this.observable.notifyObservers(exception); 356 | } 357 | 358 | @Override 359 | public void onResponse(OAuthResponse response) { 360 | this.observable.notifyObservers(response); 361 | } 362 | 363 | @Override 364 | public boolean removeObserver(OAuthRequestObserver observer) { 365 | return this.observable.removeObserver(observer); 366 | } 367 | 368 | /** 369 | * Gets the display parameter by looking at the screen size of the activity. 370 | * @return "android_phone" for phones and "android_tablet" for tablets. 371 | */ 372 | private String getDisplayParameter() { 373 | ScreenSize screenSize = ScreenSize.determineScreenSize(this.activity); 374 | DeviceType deviceType = screenSize.getDeviceType(); 375 | 376 | return deviceType.getDisplayParameter().toString().toLowerCase(Locale.US); 377 | } 378 | 379 | /** 380 | * Called when the response uri contains an access_token in the fragment. 381 | * 382 | * This method reads the response and calls back the LiveOAuthListener on the UI/main thread, 383 | * and then dismisses the dialog window. 384 | * 385 | * See Section 386 | * 1.3.1 of the OAuth 2.0 spec. 387 | * 388 | * @param fragmentParameters in the uri 389 | */ 390 | private void onAccessTokenResponse(Map fragmentParameters) { 391 | if (fragmentParameters == null) throw new AssertionError(); 392 | 393 | OAuthSuccessfulResponse response; 394 | try { 395 | response = OAuthSuccessfulResponse.createFromFragment(fragmentParameters); 396 | } catch (LiveAuthException e) { 397 | this.onException(e); 398 | return; 399 | } 400 | 401 | this.onResponse(response); 402 | } 403 | 404 | /** 405 | * Called when the response uri contains an authorization code. 406 | * 407 | * This method launches an async AccessTokenRequest and dismisses the dialog window. 408 | * 409 | * See Section 410 | * 4.1.2 of the OAuth 2.0 spec for more information. 411 | * 412 | * @param code is the authorization code from the uri 413 | */ 414 | private void onAuthorizationResponse(String code) { 415 | if (TextUtils.isEmpty(code)) throw new AssertionError(); 416 | 417 | // Since we DO have an authorization code, launch an AccessTokenRequest. 418 | // We do this asynchronously to prevent the HTTP IO from occupying the 419 | // UI/main thread (which we are on right now). 420 | AccessTokenRequest request = new AccessTokenRequest(this.client, 421 | this.clientId, 422 | code, 423 | mOAuthConfig); 424 | 425 | TokenRequestAsync requestAsync = new TokenRequestAsync(request); 426 | // We want to know when this request finishes, because we need to notify our 427 | // observers. 428 | requestAsync.addObserver(this); 429 | requestAsync.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 430 | } 431 | 432 | /** 433 | * Called when the end uri is loaded. 434 | * 435 | * This method will read the uri's query parameters and fragment, and respond with the 436 | * appropriate action. 437 | * 438 | * @param endUri that was loaded 439 | */ 440 | private void onEndUri(Uri endUri) { 441 | // If we are on an end uri, the response could either be in 442 | // the fragment or the query parameters. The response could 443 | // either be successful or it could contain an error. 444 | // Check all situations and call the listener's appropriate callback. 445 | // Callback the listener on the UI/main thread. We could call it right away since 446 | // we are on the UI/main thread, but it is probably better that we finish up with 447 | // the WebView code before we callback on the listener. 448 | boolean hasFragment = endUri.getFragment() != null; 449 | boolean hasQueryParameters = endUri.getQuery() != null; 450 | boolean invalidUri = !hasFragment && !hasQueryParameters; 451 | boolean isHierarchical = endUri.isHierarchical(); 452 | 453 | // check for an invalid uri, and leave early 454 | if (invalidUri) { 455 | this.onInvalidUri(); 456 | return; 457 | } 458 | 459 | if (hasFragment) { 460 | Map fragmentParameters = 461 | AuthorizationRequest.getFragmentParametersMap(endUri); 462 | 463 | boolean isSuccessfulResponse = 464 | fragmentParameters.containsKey(OAuth.ACCESS_TOKEN) && 465 | fragmentParameters.containsKey(OAuth.TOKEN_TYPE); 466 | if (isSuccessfulResponse) { 467 | this.onAccessTokenResponse(fragmentParameters); 468 | return; 469 | } 470 | 471 | String error = fragmentParameters.get(OAuth.ERROR); 472 | if (error != null) { 473 | String errorDescription = fragmentParameters.get(OAuth.ERROR_DESCRIPTION); 474 | String errorUri = fragmentParameters.get(OAuth.ERROR_URI); 475 | this.onError(error, errorDescription, errorUri); 476 | return; 477 | } 478 | } 479 | 480 | if (hasQueryParameters && isHierarchical) { 481 | String code = endUri.getQueryParameter(OAuth.CODE); 482 | if (code != null) { 483 | this.onAuthorizationResponse(code); 484 | return; 485 | } 486 | 487 | String error = endUri.getQueryParameter(OAuth.ERROR); 488 | if (error != null) { 489 | String errorDescription = endUri.getQueryParameter(OAuth.ERROR_DESCRIPTION); 490 | String errorUri = endUri.getQueryParameter(OAuth.ERROR_URI); 491 | this.onError(error, errorDescription, errorUri); 492 | return; 493 | } 494 | } 495 | 496 | if (hasQueryParameters && !isHierarchical) { 497 | String[] pairs = endUri.getQuery().split("&|="); 498 | for (int i = 0; i < pairs.length; i =+ 2) { 499 | if (pairs[i].equals(OAuth.CODE)) { 500 | this.onAuthorizationResponse(pairs[i + 1]); 501 | return; 502 | } 503 | } 504 | } 505 | 506 | // if the code reaches this point, the uri was invalid 507 | // because it did not contain either a successful response 508 | // or an error in either the queryParameter or the fragment 509 | this.onInvalidUri(); 510 | } 511 | 512 | /** 513 | * Called when end uri had an error in either the fragment or the query parameter. 514 | * 515 | * This method constructs the proper exception, calls the listener's appropriate callback method 516 | * on the main/UI thread, and then dismisses the dialog window. 517 | * 518 | * @param error containing an error code 519 | * @param errorDescription optional text with additional information 520 | * @param errorUri optional uri that is associated with the error. 521 | */ 522 | private void onError(String error, String errorDescription, String errorUri) { 523 | LiveAuthException exception = new LiveAuthException(error, 524 | errorDescription, 525 | errorUri); 526 | this.onException(exception); 527 | } 528 | 529 | /** 530 | * Called when an invalid uri (i.e., a uri that does not contain an error or a successful 531 | * response). 532 | * 533 | * This method constructs an exception, calls the listener's appropriate callback on the main/UI 534 | * thread, and then dismisses the dialog window. 535 | */ 536 | private void onInvalidUri() { 537 | LiveAuthException exception = new LiveAuthException(ErrorMessages.SERVER_ERROR); 538 | this.onException(exception); 539 | } 540 | } 541 | -------------------------------------------------------------------------------- /src/main/java/com/microsoft/services/msa/Config.java: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) 2014 Microsoft Corporation 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // ------------------------------------------------------------------------------ 22 | 23 | package com.microsoft.services.msa; 24 | 25 | import android.net.Uri; 26 | import android.text.TextUtils; 27 | 28 | /** 29 | * Config is a singleton class that contains the values used throughout the SDK. 30 | */ 31 | enum Config { 32 | INSTANCE; 33 | 34 | private Uri apiUri; 35 | private String apiVersion; 36 | 37 | Config() { 38 | // initialize default values for constants 39 | apiUri = Uri.parse("https://apis.live.net/v5.0"); 40 | apiVersion = "5.0"; 41 | } 42 | 43 | public Uri getApiUri() { 44 | return apiUri; 45 | } 46 | 47 | public String getApiVersion() { 48 | return apiVersion; 49 | } 50 | 51 | public void setApiUri(Uri apiUri) { 52 | assert apiUri != null; 53 | this.apiUri = apiUri; 54 | } 55 | 56 | public void setApiVersion(String apiVersion) { 57 | if (TextUtils.isEmpty(apiVersion)) throw new AssertionError(); 58 | this.apiVersion = apiVersion; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/microsoft/services/msa/DefaultObservableOAuthRequest.java: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) 2014 Microsoft Corporation 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // ------------------------------------------------------------------------------ 22 | 23 | package com.microsoft.services.msa; 24 | 25 | import java.util.ArrayList; 26 | import java.util.List; 27 | 28 | /** 29 | * Default implementation of an ObserverableOAuthRequest. 30 | * Other classes that need to be observed can compose themselves out of this class. 31 | */ 32 | class DefaultObservableOAuthRequest implements ObservableOAuthRequest { 33 | 34 | private final List observers; 35 | 36 | public DefaultObservableOAuthRequest() { 37 | this.observers = new ArrayList(); 38 | } 39 | 40 | @Override 41 | public void addObserver(OAuthRequestObserver observer) { 42 | this.observers.add(observer); 43 | } 44 | 45 | /** 46 | * Calls all the Observerable's observer's onException. 47 | * 48 | * @param exception to give to the observers 49 | */ 50 | public void notifyObservers(LiveAuthException exception) { 51 | for (final OAuthRequestObserver observer : this.observers) { 52 | observer.onException(exception); 53 | } 54 | } 55 | 56 | /** 57 | * Calls all this Observable's observer's onResponse. 58 | * 59 | * @param response to give to the observers 60 | */ 61 | public void notifyObservers(OAuthResponse response) { 62 | for (final OAuthRequestObserver observer : this.observers) { 63 | observer.onResponse(response); 64 | } 65 | } 66 | 67 | @Override 68 | public boolean removeObserver(OAuthRequestObserver observer) { 69 | return this.observers.remove(observer); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/com/microsoft/services/msa/DeviceType.java: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) 2014 Microsoft Corporation 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // ------------------------------------------------------------------------------ 22 | 23 | package com.microsoft.services.msa; 24 | 25 | /** 26 | * The type of the device is used to determine the display query parameter for login.live.com. 27 | * Phones have a display parameter of android_phone. 28 | * Tablets have a display parameter of android_tablet. 29 | */ 30 | enum DeviceType { 31 | PHONE { 32 | @Override 33 | public OAuth.DisplayType getDisplayParameter() { 34 | return OAuth.DisplayType.ANDROID_PHONE; 35 | } 36 | }, 37 | TABLET { 38 | @Override 39 | public OAuth.DisplayType getDisplayParameter() { 40 | return OAuth.DisplayType.ANDROID_TABLET; 41 | } 42 | }; 43 | 44 | abstract public OAuth.DisplayType getDisplayParameter(); 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/microsoft/services/msa/ErrorMessages.java: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) 2014 Microsoft Corporation 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // ------------------------------------------------------------------------------ 22 | 23 | package com.microsoft.services.msa; 24 | 25 | /** 26 | * ErrorMessages is a non-instantiable class that contains all the String constants 27 | * used in for errors and exceptions. 28 | */ 29 | final class ErrorMessages { 30 | public static final String ABSOLUTE_PARAMETER = 31 | "Input parameter '%1$s' is invalid. '%1$s' cannot be absolute."; 32 | public static final String CLIENT_ERROR = 33 | "An error occured on the client during the operation."; 34 | public static final String EMPTY_PARAMETER = 35 | "Input parameter '%1$s' is invalid. '%1$s' cannot be empty."; 36 | public static final String INVALID_URI = 37 | "Input parameter '%1$s' is invalid. '%1$s' must be a valid URI."; 38 | public static final String LOGGED_OUT = "The user has is logged out."; 39 | public static final String LOGIN_IN_PROGRESS = 40 | "Another login operation is already in progress."; 41 | public static final String MISSING_UPLOAD_LOCATION = 42 | "The provided path does not contain an upload_location."; 43 | public static final String NON_INSTANTIABLE_CLASS = "Non-instantiable class"; 44 | public static final String NULL_PARAMETER = 45 | "Input parameter '%1$s' is invalid. '%1$s' cannot be null."; 46 | public static final String SERVER_ERROR = 47 | "An error occured while communicating with the server during the operation. " + 48 | "Please try again later."; 49 | public static final String SIGNIN_CANCEL = "The user cancelled the login operation."; 50 | 51 | private ErrorMessages() { throw new AssertionError(NON_INSTANTIABLE_CLASS); } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/microsoft/services/msa/LiveAuthClient.java: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) 2014 Microsoft Corporation 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // ------------------------------------------------------------------------------ 22 | 23 | package com.microsoft.services.msa; 24 | 25 | import android.app.Activity; 26 | import android.app.Dialog; 27 | import android.content.Context; 28 | import android.content.SharedPreferences; 29 | import android.content.SharedPreferences.Editor; 30 | import android.net.Uri; 31 | import android.os.AsyncTask; 32 | import android.os.Build; 33 | import android.text.TextUtils; 34 | import android.util.Log; 35 | import android.webkit.CookieManager; 36 | import android.webkit.CookieSyncManager; 37 | 38 | import org.apache.http.client.HttpClient; 39 | import org.apache.http.impl.client.DefaultHttpClient; 40 | 41 | import java.util.Arrays; 42 | import java.util.Collections; 43 | import java.util.HashSet; 44 | import java.util.List; 45 | import java.util.Locale; 46 | import java.util.Set; 47 | 48 | /** 49 | * {@code LiveAuthClient} is a class responsible for retrieving a {@link LiveConnectSession}, which 50 | * provides authentication information for calls to Live APIs. 51 | */ 52 | public class LiveAuthClient { 53 | 54 | private static final String TAG = "LiveAuthClient"; 55 | 56 | private static class AuthCompleteRunnable extends AuthListenerCaller implements Runnable { 57 | 58 | private final LiveStatus status; 59 | private final LiveConnectSession session; 60 | 61 | public AuthCompleteRunnable(LiveAuthListener listener, 62 | Object userState, 63 | LiveStatus status, 64 | LiveConnectSession session) { 65 | super(listener, userState); 66 | this.status = status; 67 | this.session = session; 68 | } 69 | 70 | @Override 71 | public void run() { 72 | listener.onAuthComplete(status, session, userState); 73 | } 74 | } 75 | 76 | private static class AuthErrorRunnable extends AuthListenerCaller implements Runnable { 77 | 78 | private final LiveAuthException exception; 79 | 80 | public AuthErrorRunnable(LiveAuthListener listener, 81 | Object userState, 82 | LiveAuthException exception) { 83 | super(listener, userState); 84 | this.exception = exception; 85 | } 86 | 87 | @Override 88 | public void run() { 89 | listener.onAuthError(exception, userState); 90 | } 91 | 92 | } 93 | 94 | private static abstract class AuthListenerCaller { 95 | protected final LiveAuthListener listener; 96 | protected final Object userState; 97 | 98 | public AuthListenerCaller(LiveAuthListener listener, Object userState) { 99 | this.listener = listener; 100 | this.userState = userState; 101 | } 102 | } 103 | 104 | /** 105 | * This class observes an {@link AccessTokenRequest} and calls the appropriate Listener method. 106 | * On a successful response, it will call the 107 | * {@link LiveAuthListener#onAuthComplete(LiveStatus, LiveConnectSession, Object)}. 108 | * On an exception or an unsuccessful response, it will call 109 | * {@link LiveAuthListener#onAuthError(LiveAuthException, Object)}. 110 | */ 111 | private class ListenerCallerObserver extends AuthListenerCaller 112 | implements OAuthRequestObserver, 113 | OAuthResponseVisitor { 114 | 115 | public ListenerCallerObserver(LiveAuthListener listener, Object userState) { 116 | super(listener, userState); 117 | } 118 | 119 | @Override 120 | public void onException(LiveAuthException exception) { 121 | new AuthErrorRunnable(listener, userState, exception).run(); 122 | } 123 | 124 | @Override 125 | public void onResponse(OAuthResponse response) { 126 | response.accept(this); 127 | } 128 | 129 | @Override 130 | public void visit(OAuthErrorResponse response) { 131 | String error = response.getError().toString().toLowerCase(Locale.US); 132 | String errorDescription = response.getErrorDescription(); 133 | String errorUri = response.getErrorUri(); 134 | LiveAuthException exception = new LiveAuthException(error, 135 | errorDescription, 136 | errorUri); 137 | 138 | new AuthErrorRunnable(listener, userState, exception).run(); 139 | } 140 | 141 | @Override 142 | public void visit(OAuthSuccessfulResponse response) { 143 | session.loadFromOAuthResponse(response); 144 | 145 | new AuthCompleteRunnable(listener, userState, LiveStatus.CONNECTED, session).run(); 146 | } 147 | } 148 | 149 | /** Observer that will, depending on the response, save or clear the refresh token. */ 150 | private class RefreshTokenWriter implements OAuthRequestObserver, OAuthResponseVisitor { 151 | 152 | @Override 153 | public void onException(LiveAuthException exception) { } 154 | 155 | @Override 156 | public void onResponse(OAuthResponse response) { 157 | response.accept(this); 158 | } 159 | 160 | @Override 161 | public void visit(OAuthErrorResponse response) { 162 | if (response.getError() == OAuth.ErrorType.INVALID_GRANT) { 163 | LiveAuthClient.this.clearRefreshTokenFromPreferences(); 164 | } 165 | } 166 | 167 | @Override 168 | public void visit(OAuthSuccessfulResponse response) { 169 | String refreshToken = response.getRefreshToken(); 170 | if (!TextUtils.isEmpty(refreshToken)) { 171 | this.saveRefreshTokenToPreferences(refreshToken); 172 | } 173 | } 174 | 175 | private boolean saveRefreshTokenToPreferences(String refreshToken) { 176 | if (TextUtils.isEmpty(refreshToken)) throw new AssertionError(); 177 | 178 | SharedPreferences settings = 179 | applicationContext.getSharedPreferences(PreferencesConstants.FILE_NAME, 180 | Context.MODE_PRIVATE); 181 | Editor editor = settings.edit(); 182 | editor.putString(PreferencesConstants.REFRESH_TOKEN_KEY, refreshToken); 183 | 184 | return editor.commit(); 185 | } 186 | } 187 | 188 | /** 189 | * An {@link OAuthResponseVisitor} that checks the {@link OAuthResponse} and if it is a 190 | * successful response, it loads the response into the given session. 191 | */ 192 | private static class SessionRefresher implements OAuthResponseVisitor { 193 | 194 | private final LiveConnectSession session; 195 | private boolean visitedSuccessfulResponse; 196 | 197 | public SessionRefresher(LiveConnectSession session) { 198 | if (session == null) throw new AssertionError(); 199 | 200 | this.session = session; 201 | this.visitedSuccessfulResponse = false; 202 | } 203 | 204 | @Override 205 | public void visit(OAuthErrorResponse response) { 206 | this.visitedSuccessfulResponse = false; 207 | } 208 | 209 | @Override 210 | public void visit(OAuthSuccessfulResponse response) { 211 | this.session.loadFromOAuthResponse(response); 212 | this.visitedSuccessfulResponse = true; 213 | } 214 | 215 | public boolean visitedSuccessfulResponse() { 216 | return this.visitedSuccessfulResponse; 217 | } 218 | } 219 | 220 | /** 221 | * A LiveAuthListener that does nothing on each of the call backs. 222 | * This is used so when a null listener is passed in, this can be used, instead of null, 223 | * to avoid if (listener == null) checks. 224 | */ 225 | private static final LiveAuthListener NULL_LISTENER = new LiveAuthListener() { 226 | @Override 227 | public void onAuthComplete(LiveStatus status, LiveConnectSession session, Object sender) { } 228 | @Override 229 | public void onAuthError(LiveAuthException exception, Object sender) { } 230 | }; 231 | 232 | private final Context applicationContext; 233 | private final String clientId; 234 | private boolean hasPendingLoginRequest; 235 | 236 | /** 237 | * Responsible for all network (i.e., HTTP) calls. 238 | * Tests will want to change this to mock the network and HTTP responses. 239 | * @see #setHttpClient(HttpClient) 240 | */ 241 | private HttpClient httpClient; 242 | 243 | /** saved from initialize and used in the login call if login's scopes are null. */ 244 | private Set baseScopes; 245 | 246 | /** The OAuth configuration */ 247 | private final OAuthConfig mOAuthConfig; 248 | 249 | /** One-to-one relationship between LiveAuthClient and LiveConnectSession. */ 250 | private final LiveConnectSession session; 251 | 252 | { 253 | this.httpClient = new DefaultHttpClient(); 254 | this.hasPendingLoginRequest = false; 255 | this.session = new LiveConnectSession(this); 256 | } 257 | 258 | /** 259 | * Constructs a new {@code LiveAuthClient} instance and initializes its member variables. 260 | * 261 | * @param context Context of the Application used to save any refresh_token. 262 | * @param clientId The client_id of the Live Connect Application to login to. 263 | * @param scopes to initialize the {@link LiveConnectSession} with. 264 | * See MSDN Live Connect 265 | * Reference's Scopes and permissions for a list of scopes and explanations. 266 | */ 267 | public LiveAuthClient(final Context context, 268 | final String clientId, 269 | final Iterable scopes, 270 | final OAuthConfig oAuthConfig) { 271 | LiveConnectUtils.assertNotNull(context, "context"); 272 | LiveConnectUtils.assertNotNullOrEmpty(clientId, "clientId"); 273 | 274 | this.applicationContext = context.getApplicationContext(); 275 | this.clientId = clientId; 276 | 277 | 278 | if (oAuthConfig == null) { 279 | this.mOAuthConfig = MicrosoftOAuthConfig.getInstance(); 280 | } else { 281 | this.mOAuthConfig = oAuthConfig; 282 | } 283 | 284 | // copy scopes for login 285 | Iterable tempScopes = scopes; 286 | if (tempScopes == null) { 287 | tempScopes = Arrays.asList(new String[0]); 288 | } 289 | 290 | this.baseScopes = new HashSet<>(); 291 | for (final String scope : tempScopes) { 292 | this.baseScopes.add(scope); 293 | } 294 | 295 | this.baseScopes = Collections.unmodifiableSet(this.baseScopes); 296 | 297 | final String refreshToken = this.getRefreshTokenFromPreferences(); 298 | if (!TextUtils.isEmpty(refreshToken)) { 299 | final String scopeAsString = TextUtils.join(OAuth.SCOPE_DELIMITER, this.baseScopes); 300 | RefreshAccessTokenRequest request = new RefreshAccessTokenRequest(this.httpClient, 301 | this.clientId, 302 | refreshToken, 303 | scopeAsString, 304 | this.mOAuthConfig); 305 | TokenRequestAsync requestAsync = new TokenRequestAsync(request); 306 | requestAsync.addObserver(new RefreshTokenWriter()); 307 | requestAsync.execute(); 308 | } 309 | } 310 | 311 | public LiveAuthClient(final Context context, final String clientId) { 312 | this(context, clientId, null); 313 | } 314 | 315 | public LiveAuthClient(final Context context, 316 | final String clientId, 317 | final Iterable scopes) { 318 | this(context,clientId, scopes, null); 319 | } 320 | 321 | /** @return the client_id of the Live Connect application. */ 322 | public String getClientId() { 323 | return this.clientId; 324 | } 325 | 326 | public void login(Activity activity, LiveAuthListener listener) { 327 | this.login(activity, null, null, listener); 328 | } 329 | 330 | public void login(Activity activity, Iterable scopes, LiveAuthListener listener) { 331 | this.login(activity, scopes, null, null, listener); 332 | } 333 | 334 | public void login(Activity activity, Iterable scopes, Object userState, LiveAuthListener listener) { 335 | this.login(activity, scopes, userState, null, listener); 336 | } 337 | 338 | /** 339 | * Logs in an user with the given scopes and additional saved state. 340 | * 341 | * login displays a {@link Dialog} that will prompt the 342 | * user for a username and password, and ask for consent to use the given scopes. 343 | * A {@link LiveConnectSession} will be returned by calling 344 | * {@link LiveAuthListener#onAuthComplete(LiveStatus, LiveConnectSession, Object)}. 345 | * Otherwise, the {@link LiveAuthListener#onAuthError(LiveAuthException, Object)} will be 346 | * called. These methods will be called on the main/UI thread. 347 | * 348 | * @param activity {@link Activity} instance to display the Login dialog on 349 | * @param scopes to initialize the {@link LiveConnectSession} with. 350 | * See MSDN Live Connect 351 | * Reference's Scopes and permissions for a list of scopes and explanations. 352 | * Scopes specified here override scopes specified in the constructor. 353 | * @param userState arbitrary object that is used to determine the caller of the method. 354 | * @param loginHint the hint for the sign in experience to show the username pre-filled out 355 | * @param listener called on either completion or error during the login process. 356 | * @throws IllegalStateException if there is a pending login request. 357 | */ 358 | public void login(Activity activity, 359 | Iterable scopes, 360 | Object userState, 361 | String loginHint, 362 | LiveAuthListener listener 363 | ) { 364 | LiveConnectUtils.assertNotNull(activity, "activity"); 365 | 366 | if (listener == null) { 367 | listener = NULL_LISTENER; 368 | } 369 | 370 | if (this.hasPendingLoginRequest) { 371 | throw new IllegalStateException(ErrorMessages.LOGIN_IN_PROGRESS); 372 | } 373 | 374 | // if no scopes were passed in, use the scopes from initialize or if those are empty, 375 | // create an empty list 376 | if (scopes == null) { 377 | if (this.baseScopes == null) { 378 | scopes = Arrays.asList(new String[0]); 379 | } else { 380 | scopes = this.baseScopes; 381 | } 382 | } 383 | 384 | // if the session is valid and contains all the scopes, do not display the login ui. 385 | if (loginSilent(scopes, userState, listener)) { 386 | Log.i(TAG, "Interactive login not required."); 387 | return; 388 | } 389 | 390 | // silent login failed, initiating interactive login 391 | String scope = TextUtils.join(OAuth.SCOPE_DELIMITER, scopes); 392 | 393 | AuthorizationRequest request = new AuthorizationRequest(activity, 394 | this.httpClient, 395 | this.clientId, 396 | scope, 397 | loginHint, 398 | mOAuthConfig); 399 | 400 | request.addObserver(new ListenerCallerObserver(listener, userState)); 401 | request.addObserver(new RefreshTokenWriter()); 402 | request.addObserver(new OAuthRequestObserver() { 403 | @Override 404 | public void onException(LiveAuthException exception) { 405 | LiveAuthClient.this.hasPendingLoginRequest = false; 406 | } 407 | 408 | @Override 409 | public void onResponse(OAuthResponse response) { 410 | LiveAuthClient.this.hasPendingLoginRequest = false; 411 | } 412 | }); 413 | 414 | this.hasPendingLoginRequest = true; 415 | 416 | request.execute(); 417 | } 418 | 419 | public Boolean loginSilent(LiveAuthListener listener) { 420 | return this.loginSilent(null, null, listener); 421 | } 422 | 423 | public Boolean loginSilent(Iterable scopes, LiveAuthListener listener) { 424 | return this.loginSilent(scopes, null, listener); 425 | } 426 | 427 | public Boolean loginSilent(Object userState, LiveAuthListener listener) { 428 | return this.loginSilent(null, userState, listener); 429 | } 430 | 431 | /** 432 | * Attempts to log in a user using multiple non-interactive approaches. 433 | * 434 | * A {@link LiveConnectSession} will be returned by calling 435 | * {@link LiveAuthListener#onAuthComplete(LiveStatus, LiveConnectSession, Object)}. 436 | * Otherwise, the {@link LiveAuthListener#onAuthError(LiveAuthException, Object)} will be 437 | * called. These methods will be called on the main/UI thread. 438 | * 439 | * @param scopes list of scopes which will override scopes from constructor 440 | * @param userState state object that is pass to listener on completion. 441 | * @param listener called on either completion or error during the login process. 442 | * @return false == silent login failed, interactive login required. 443 | * true == silent login is continuing on the background thread the listener will be 444 | * called back when it has completed. 445 | */ 446 | public Boolean loginSilent(final Iterable scopes, 447 | final Object userState, 448 | final LiveAuthListener listener) { 449 | 450 | if (this.hasPendingLoginRequest) { 451 | throw new IllegalStateException(ErrorMessages.LOGIN_IN_PROGRESS); 452 | } 453 | 454 | final Iterable activeScopes; 455 | if (scopes == null) { 456 | if (this.baseScopes == null) { 457 | activeScopes = Arrays.asList(new String[0]); 458 | } else { 459 | activeScopes = this.baseScopes; 460 | } 461 | } else { 462 | activeScopes = scopes; 463 | } 464 | 465 | if (TextUtils.isEmpty(this.session.getRefreshToken())) { 466 | this.session.setRefreshToken(getRefreshTokenFromPreferences()); 467 | } 468 | 469 | // if the session is valid and contains all the scopes, do not display the login ui. 470 | final boolean needNewAccessToken = this.session.isExpired() || !this.session.contains(activeScopes); 471 | final boolean attemptingToLoginSilently = TextUtils.isEmpty(this.session.getRefreshToken()); 472 | 473 | new AsyncTask() { 474 | @Override 475 | protected Void doInBackground(final Void... voids) { 476 | if (!needNewAccessToken) { 477 | Log.i(TAG, "Access token still valid, so using it."); 478 | listener.onAuthComplete(LiveStatus.CONNECTED, LiveAuthClient.this.session, userState); 479 | } else if (tryRefresh(activeScopes)) { 480 | Log.i(TAG, "Used refresh token to refresh access and refresh tokens."); 481 | listener.onAuthComplete(LiveStatus.CONNECTED, LiveAuthClient.this.session, userState); 482 | } else { 483 | Log.i(TAG, "All tokens expired, you need to call login() to initiate interactive logon"); 484 | listener.onAuthComplete(LiveStatus.NOT_CONNECTED, 485 | LiveAuthClient.this.getSession(), userState); 486 | } 487 | return null; 488 | } 489 | }.execute(); 490 | 491 | return !attemptingToLoginSilently; 492 | } 493 | 494 | /** 495 | * Logs out the given user. 496 | * 497 | * Also, this method clears the previously created {@link LiveConnectSession}. 498 | * {@link LiveAuthListener#onAuthComplete(LiveStatus, LiveConnectSession, Object)} will be 499 | * called on completion. Otherwise, 500 | * {@link LiveAuthListener#onAuthError(LiveAuthException, Object)} will be called. 501 | * 502 | * @param listener called on either completion or error during the logout process. 503 | */ 504 | public void logout(LiveAuthListener listener) { 505 | this.logout(null, listener); 506 | } 507 | 508 | /** 509 | * Logs out the given user. 510 | * 511 | * Also, this method clears the previously created {@link LiveConnectSession}. 512 | * {@link LiveAuthListener#onAuthComplete(LiveStatus, LiveConnectSession, Object)} will be 513 | * called on completion. Otherwise, 514 | * {@link LiveAuthListener#onAuthError(LiveAuthException, Object)} will be called. 515 | * 516 | * @param userState arbitrary object that is used to determine the caller of the method. 517 | * @param listener called on either completion or error during the logout process. 518 | */ 519 | public void logout(Object userState, LiveAuthListener listener) { 520 | if (listener == null) { 521 | listener = NULL_LISTENER; 522 | } 523 | 524 | session.setAccessToken(null); 525 | session.setAuthenticationToken(null); 526 | session.setRefreshToken(null); 527 | session.setScopes(null); 528 | session.setTokenType(null); 529 | 530 | clearRefreshTokenFromPreferences(); 531 | 532 | CookieSyncManager cookieSyncManager = 533 | CookieSyncManager.createInstance(this.applicationContext); 534 | CookieManager manager = CookieManager.getInstance(); 535 | 536 | // clear cookies to force prompt on login 537 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) 538 | manager.removeAllCookies(null); 539 | else 540 | manager.removeAllCookie(); 541 | 542 | cookieSyncManager.sync(); 543 | listener.onAuthComplete(LiveStatus.UNKNOWN, null, userState); 544 | } 545 | 546 | /** @return The {@link HttpClient} instance used by this {@code LiveAuthClient}. */ 547 | HttpClient getHttpClient() { 548 | return this.httpClient; 549 | } 550 | 551 | /** @return The {@link LiveConnectSession} instance that this {@code LiveAuthClient} created. */ 552 | public LiveConnectSession getSession() { 553 | return session; 554 | } 555 | 556 | /** 557 | * Refreshes the previously created session. 558 | * 559 | * @return true if the session was successfully refreshed. 560 | */ 561 | Boolean tryRefresh(Iterable scopes) { 562 | 563 | String scope = TextUtils.join(OAuth.SCOPE_DELIMITER, scopes); 564 | String refreshToken = this.session.getRefreshToken(); 565 | 566 | if (TextUtils.isEmpty(refreshToken)) { 567 | Log.i(TAG, "No refresh token available, sorry!"); 568 | return false; 569 | } 570 | 571 | Log.i(TAG, "Refresh token found, attempting to refresh access and refresh tokens."); 572 | RefreshAccessTokenRequest request = 573 | new RefreshAccessTokenRequest(this.httpClient, this.clientId, refreshToken, scope, mOAuthConfig); 574 | 575 | OAuthResponse response; 576 | try { 577 | response = request.execute(); 578 | } catch (LiveAuthException e) { 579 | return false; 580 | } 581 | 582 | SessionRefresher refresher = new SessionRefresher(this.session); 583 | response.accept(refresher); 584 | response.accept(new RefreshTokenWriter()); 585 | 586 | return refresher.visitedSuccessfulResponse(); 587 | } 588 | 589 | /** 590 | * Sets the {@link HttpClient} that is used for HTTP requests by this {@code LiveAuthClient}. 591 | * Tests will want to change this to mock the network/HTTP responses. 592 | * @param client The new HttpClient to be set. 593 | */ 594 | void setHttpClient(HttpClient client) { 595 | if (client == null) throw new AssertionError(); 596 | this.httpClient = client; 597 | } 598 | 599 | /** 600 | * Clears the refresh token from this {@code LiveAuthClient}'s 601 | * {@link Activity#getPreferences(int)}. 602 | * 603 | * @return true if the refresh token was successfully cleared. 604 | */ 605 | private boolean clearRefreshTokenFromPreferences() { 606 | SharedPreferences settings = getSharedPreferences(); 607 | Editor editor = settings.edit(); 608 | editor.remove(PreferencesConstants.REFRESH_TOKEN_KEY); 609 | 610 | return editor.commit(); 611 | } 612 | 613 | private SharedPreferences getSharedPreferences() { 614 | return applicationContext.getSharedPreferences(PreferencesConstants.FILE_NAME, 615 | Context.MODE_PRIVATE); 616 | } 617 | 618 | private List getCookieKeysFromPreferences() { 619 | SharedPreferences settings = getSharedPreferences(); 620 | String cookieKeys = settings.getString(PreferencesConstants.COOKIES_KEY, ""); 621 | 622 | return Arrays.asList(TextUtils.split(cookieKeys, PreferencesConstants.COOKIE_DELIMITER)); 623 | } 624 | 625 | /** 626 | * Retrieves the refresh token from this {@code LiveAuthClient}'s 627 | * {@link Activity#getPreferences(int)}. 628 | * 629 | * @return the refresh token from persistent storage. 630 | */ 631 | private String getRefreshTokenFromPreferences() { 632 | SharedPreferences settings = getSharedPreferences(); 633 | return settings.getString(PreferencesConstants.REFRESH_TOKEN_KEY, null); 634 | } 635 | } 636 | -------------------------------------------------------------------------------- /src/main/java/com/microsoft/services/msa/LiveAuthException.java: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) 2014 Microsoft Corporation 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // ------------------------------------------------------------------------------ 22 | 23 | package com.microsoft.services.msa; 24 | 25 | /** 26 | * Indicates that an exception occurred during the Auth process. 27 | */ 28 | public class LiveAuthException extends RuntimeException { 29 | 30 | private static final long serialVersionUID = 3368677530670470856L; 31 | 32 | private final String error; 33 | private final String errorUri; 34 | 35 | 36 | public LiveAuthException(String errorMessage) { 37 | super(errorMessage); 38 | this.error = ""; 39 | this.errorUri = ""; 40 | } 41 | 42 | public LiveAuthException(String errorMessage, Throwable throwable) { 43 | super(errorMessage, throwable); 44 | this.error = ""; 45 | this.errorUri = ""; 46 | } 47 | 48 | public LiveAuthException(String error, String errorDescription, String errorUri) { 49 | super(errorDescription); 50 | 51 | if (error == null) throw new AssertionError(); 52 | 53 | this.error = error; 54 | this.errorUri = errorUri; 55 | } 56 | 57 | public LiveAuthException(String error, String errorDescription, String errorUri, Throwable cause) { 58 | super(errorDescription, cause); 59 | 60 | if (error == null) throw new AssertionError(); 61 | 62 | this.error = error; 63 | this.errorUri = errorUri; 64 | } 65 | 66 | /** 67 | * @return Returns the authentication error. 68 | */ 69 | public String getError() { 70 | return this.error; 71 | } 72 | 73 | /** 74 | * @return Returns the error URI. 75 | */ 76 | public String getErrorUri() { 77 | return this.errorUri; 78 | } 79 | } 80 | 81 | -------------------------------------------------------------------------------- /src/main/java/com/microsoft/services/msa/LiveAuthListener.java: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) 2014 Microsoft Corporation 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // ------------------------------------------------------------------------------ 22 | 23 | package com.microsoft.services.msa; 24 | 25 | /** 26 | * Handles callback methods for LiveAuthClient init, login, and logout methods. 27 | * Returns the * status of the operation when onAuthComplete is called. If there was an error 28 | * during the operation, onAuthError is called with the exception that was thrown. 29 | */ 30 | public interface LiveAuthListener { 31 | 32 | /** 33 | * Invoked when the operation completes successfully. 34 | * 35 | * @param status The {@link LiveStatus} for an operation. If successful, the status is 36 | * CONNECTED. If unsuccessful, NOT_CONNECTED or UNKNOWN are returned. 37 | * @param session The {@link LiveConnectSession} from the {@link LiveAuthClient}. 38 | * @param userState An arbitrary object that is used to determine the caller of the method. 39 | */ 40 | public void onAuthComplete(LiveStatus status, LiveConnectSession session, Object userState); 41 | 42 | /** 43 | * Invoked when the method call fails. 44 | * 45 | * @param exception The {@link LiveAuthException} error. 46 | * @param userState An arbitrary object that is used to determine the caller of the method. 47 | */ 48 | public void onAuthError(LiveAuthException exception, Object userState); 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/microsoft/services/msa/LiveConnectSession.java: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) 2014 Microsoft Corporation 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // ------------------------------------------------------------------------------ 22 | 23 | package com.microsoft.services.msa; 24 | 25 | import java.beans.PropertyChangeListener; 26 | import java.beans.PropertyChangeSupport; 27 | import java.util.Arrays; 28 | import java.util.Calendar; 29 | import java.util.Collections; 30 | import java.util.Date; 31 | import java.util.HashSet; 32 | import java.util.Set; 33 | 34 | /** 35 | * Represents a Live Connect session. 36 | */ 37 | public class LiveConnectSession { 38 | 39 | private String accessToken; 40 | private String authenticationToken; 41 | 42 | /** Keeps track of all the listeners, and fires the property change events */ 43 | private final PropertyChangeSupport changeSupport; 44 | 45 | /** 46 | * The LiveAuthClient that created this object. 47 | * This is needed in order to perform a refresh request. 48 | * There is a one-to-one relationship between the LiveConnectSession and LiveAuthClient. 49 | */ 50 | private final LiveAuthClient creator; 51 | 52 | private Date expiresIn; 53 | private String refreshToken; 54 | private Set scopes; 55 | private String tokenType; 56 | 57 | /** 58 | * Constructors a new LiveConnectSession, and sets its creator to the passed in 59 | * LiveAuthClient. All other member variables are left uninitialized. 60 | * 61 | * @param creator 62 | */ 63 | LiveConnectSession(LiveAuthClient creator) { 64 | if (creator == null) throw new AssertionError(); 65 | 66 | this.creator = creator; 67 | this.changeSupport = new PropertyChangeSupport(this); 68 | } 69 | 70 | /** 71 | * Adds a {@link PropertyChangeListener} to the session that receives notification when any 72 | * property is changed. 73 | * 74 | * @param listener 75 | */ 76 | public void addPropertyChangeListener(PropertyChangeListener listener) { 77 | if (listener == null) { 78 | return; 79 | } 80 | 81 | this.changeSupport.addPropertyChangeListener(listener); 82 | } 83 | 84 | /** 85 | * Adds a {@link PropertyChangeListener} to the session that receives notification when a 86 | * specific property is changed. 87 | * 88 | * @param propertyName 89 | * @param listener 90 | */ 91 | public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) { 92 | if (listener == null) { 93 | return; 94 | } 95 | 96 | this.changeSupport.addPropertyChangeListener(propertyName, listener); 97 | } 98 | 99 | /** 100 | * @return The access token for the signed-in, connected user. 101 | */ 102 | public String getAccessToken() { 103 | return this.accessToken; 104 | } 105 | 106 | /** 107 | * @return A user-specific token that provides information to an app so that it can validate 108 | * the user. 109 | */ 110 | public String getAuthenticationToken() { 111 | return this.authenticationToken; 112 | } 113 | 114 | /** 115 | * @return The exact time when a session expires. 116 | */ 117 | public Date getExpiresIn() { 118 | // Defensive copy 119 | return new Date(this.expiresIn.getTime()); 120 | } 121 | 122 | /** 123 | * @return An array of all PropertyChangeListeners for this session. 124 | */ 125 | public PropertyChangeListener[] getPropertyChangeListeners() { 126 | return this.changeSupport.getPropertyChangeListeners(); 127 | } 128 | 129 | /** 130 | * @param propertyName 131 | * @return An array of all PropertyChangeListeners for a specific property for this session. 132 | */ 133 | public PropertyChangeListener[] getPropertyChangeListeners(String propertyName) { 134 | return this.changeSupport.getPropertyChangeListeners(propertyName); 135 | } 136 | 137 | /** 138 | * @return A user-specific refresh token that the app can use to refresh the access token. 139 | */ 140 | public String getRefreshToken() { 141 | return this.refreshToken; 142 | } 143 | 144 | /** 145 | * @return The scopes that the user has consented to. 146 | */ 147 | public Iterable getScopes() { 148 | // Defensive copy is not necessary, because this.scopes is an unmodifiableSet 149 | return this.scopes; 150 | } 151 | 152 | /** 153 | * @return The type of token. 154 | */ 155 | public String getTokenType() { 156 | return this.tokenType; 157 | } 158 | 159 | /** 160 | * @return {@code true} if the session is expired. 161 | */ 162 | public boolean isExpired() { 163 | if (this.expiresIn == null) { 164 | return true; 165 | } 166 | 167 | final Date now = new Date(); 168 | 169 | return now.after(this.expiresIn); 170 | } 171 | 172 | /** 173 | * Removes a PropertyChangeListeners on a session. 174 | * @param listener 175 | */ 176 | public void removePropertyChangeListener(PropertyChangeListener listener) { 177 | if (listener == null) { 178 | return; 179 | } 180 | 181 | this.changeSupport.removePropertyChangeListener(listener); 182 | } 183 | 184 | /** 185 | * Removes a PropertyChangeListener for a specific property on a session. 186 | * @param propertyName 187 | * @param listener 188 | */ 189 | public void removePropertyChangeListener(String propertyName, 190 | PropertyChangeListener listener) { 191 | if (listener == null) { 192 | return; 193 | } 194 | 195 | this.changeSupport.removePropertyChangeListener(propertyName, listener); 196 | } 197 | 198 | @Override 199 | public String toString() { 200 | return String.format("LiveConnectSession [accessToken=%s, authenticationToken=%s, expiresIn=%s, refreshToken=%s, scopes=%s, tokenType=%s]", 201 | this.accessToken, 202 | this.authenticationToken, 203 | this.expiresIn, 204 | this.refreshToken, 205 | this.scopes, 206 | this.tokenType); 207 | } 208 | 209 | boolean contains(Iterable scopes) { 210 | if (scopes == null) { 211 | return true; 212 | } else if (this.scopes == null) { 213 | return false; 214 | } 215 | 216 | for (String scope : scopes) { 217 | if (!this.scopes.contains(scope)) { 218 | return false; 219 | } 220 | } 221 | 222 | return true; 223 | } 224 | 225 | /** 226 | * Fills in the LiveConnectSession with the OAuthResponse. 227 | * WARNING: The OAuthResponse must not contain OAuth.ERROR. 228 | * 229 | * @param response to load from 230 | */ 231 | void loadFromOAuthResponse(OAuthSuccessfulResponse response) { 232 | this.accessToken = response.getAccessToken(); 233 | this.tokenType = response.getTokenType().toString().toLowerCase(); 234 | 235 | if (response.hasAuthenticationToken()) { 236 | this.authenticationToken = response.getAuthenticationToken(); 237 | } 238 | 239 | if (response.hasExpiresIn()) { 240 | final Calendar calendar = Calendar.getInstance(); 241 | calendar.add(Calendar.SECOND, response.getExpiresIn()); 242 | this.setExpiresIn(calendar.getTime()); 243 | } 244 | 245 | if (response.hasRefreshToken()) { 246 | this.refreshToken = response.getRefreshToken(); 247 | } 248 | 249 | if (response.hasScope()) { 250 | final String scopeString = response.getScope(); 251 | this.setScopes(Arrays.asList(scopeString.split(OAuth.SCOPE_DELIMITER))); 252 | } 253 | } 254 | 255 | /** 256 | * Refreshes this LiveConnectSession 257 | * 258 | * @return true if it was able to refresh the refresh token. 259 | */ 260 | boolean refresh() { 261 | return this.creator.tryRefresh(this.getScopes()); 262 | } 263 | 264 | void setAccessToken(String accessToken) { 265 | final String oldValue = this.accessToken; 266 | this.accessToken = accessToken; 267 | 268 | this.changeSupport.firePropertyChange("accessToken", oldValue, this.accessToken); 269 | } 270 | 271 | void setAuthenticationToken(String authenticationToken) { 272 | final String oldValue = this.authenticationToken; 273 | this.authenticationToken = authenticationToken; 274 | 275 | this.changeSupport.firePropertyChange("authenticationToken", 276 | oldValue, 277 | this.authenticationToken); 278 | } 279 | 280 | void setExpiresIn(Date expiresIn) { 281 | final Date oldValue = this.expiresIn; 282 | this.expiresIn = new Date(expiresIn.getTime()); 283 | 284 | this.changeSupport.firePropertyChange("expiresIn", oldValue, this.expiresIn); 285 | } 286 | 287 | void setRefreshToken(String refreshToken) { 288 | final String oldValue = this.refreshToken; 289 | this.refreshToken = refreshToken; 290 | 291 | this.changeSupport.firePropertyChange("refreshToken", oldValue, this.refreshToken); 292 | } 293 | 294 | void setScopes(Iterable scopes) { 295 | final Iterable oldValue = this.scopes; 296 | 297 | // Defensive copy 298 | this.scopes = new HashSet(); 299 | if (scopes != null) { 300 | for (String scope : scopes) { 301 | this.scopes.add(scope); 302 | } 303 | } 304 | 305 | this.scopes = Collections.unmodifiableSet(this.scopes); 306 | 307 | this.changeSupport.firePropertyChange("scopes", oldValue, this.scopes); 308 | } 309 | 310 | void setTokenType(String tokenType) { 311 | final String oldValue = this.tokenType; 312 | this.tokenType = tokenType; 313 | 314 | this.changeSupport.firePropertyChange("tokenType", oldValue, this.tokenType); 315 | } 316 | 317 | boolean willExpireInSecs(int secs) { 318 | final Calendar calendar = Calendar.getInstance(); 319 | calendar.add(Calendar.SECOND, secs); 320 | 321 | final Date future = calendar.getTime(); 322 | 323 | // if add secs seconds to the current time and it is after the expired time 324 | // then it is almost expired. 325 | return future.after(this.expiresIn); 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /src/main/java/com/microsoft/services/msa/LiveConnectUtils.java: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) 2014 Microsoft Corporation 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // ------------------------------------------------------------------------------ 22 | 23 | package com.microsoft.services.msa; 24 | 25 | 26 | import android.text.TextUtils; 27 | 28 | /** 29 | * LiveConnectUtils is a non-instantiable utility class that contains various helper 30 | * methods and constants. 31 | */ 32 | final class LiveConnectUtils { 33 | 34 | /** 35 | * Checks to see if the passed in Object is null, and throws a 36 | * NullPointerException if it is. 37 | * 38 | * @param object to check 39 | * @param parameterName name of the parameter that is used in the exception message 40 | * @throws NullPointerException if the Object is null 41 | */ 42 | public static void assertNotNull(Object object, String parameterName) { 43 | if (TextUtils.isEmpty(parameterName)) throw new AssertionError(); 44 | 45 | if (object == null) { 46 | final String message = String.format(ErrorMessages.NULL_PARAMETER, parameterName); 47 | throw new NullPointerException(message); 48 | } 49 | } 50 | 51 | /** 52 | * Checks to see if the passed in is an empty string, and throws an 53 | * IllegalArgumentException if it is. 54 | * 55 | * @param parameter to check 56 | * @param parameterName name of the parameter that is used in the exception message 57 | * @throws IllegalArgumentException if the parameter is empty 58 | * @throws NullPointerException if the String is null 59 | */ 60 | public static void assertNotNullOrEmpty(String parameter, String parameterName) { 61 | if (TextUtils.isEmpty(parameterName)) throw new AssertionError(); 62 | 63 | assertNotNull(parameter, parameterName); 64 | 65 | if (TextUtils.isEmpty(parameter)) { 66 | final String message = String.format(ErrorMessages.EMPTY_PARAMETER, parameterName); 67 | throw new IllegalArgumentException(message); 68 | } 69 | } 70 | 71 | /** 72 | * Private to prevent instantiation 73 | */ 74 | private LiveConnectUtils() { throw new AssertionError(ErrorMessages.NON_INSTANTIABLE_CLASS); } 75 | } 76 | 77 | -------------------------------------------------------------------------------- /src/main/java/com/microsoft/services/msa/LiveOperationException.java: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) 2014 Microsoft Corporation 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // ------------------------------------------------------------------------------ 22 | 23 | package com.microsoft.services.msa; 24 | 25 | /** 26 | * Represents errors that occur when making requests to the Representational State Transfer 27 | * (REST) API. 28 | */ 29 | public class LiveOperationException extends Exception { 30 | 31 | private static final long serialVersionUID = 4630383031651156731L; 32 | 33 | LiveOperationException(String message) { 34 | super(message); 35 | } 36 | 37 | LiveOperationException(String message, Throwable e) { 38 | super(message, e); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/microsoft/services/msa/LiveStatus.java: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) 2014 Microsoft Corporation 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // ------------------------------------------------------------------------------ 22 | 23 | package com.microsoft.services.msa; 24 | 25 | /** 26 | * Specifies the status of an auth operation. 27 | */ 28 | public enum LiveStatus { 29 | /** The status is not known. */ 30 | UNKNOWN, 31 | 32 | /** The session is connected. */ 33 | CONNECTED, 34 | 35 | /** The user has not consented to the application. */ 36 | NOT_CONNECTED; 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/microsoft/services/msa/MicrosoftOAuthConfig.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.services.msa; 2 | 3 | import android.net.Uri; 4 | 5 | /** 6 | * OAuth configuration for the Microsoft services 7 | */ 8 | public class MicrosoftOAuthConfig implements OAuthConfig { 9 | 10 | /** 11 | * The domain to authenticate against 12 | */ 13 | public static final String HTTPS_LOGIN_LIVE_COM = "https://login.live.com/"; 14 | 15 | /** 16 | * Singleton instance 17 | */ 18 | private static MicrosoftOAuthConfig sInstance; 19 | 20 | /** 21 | * The authorization uri 22 | */ 23 | private Uri mOAuthAuthorizeUri; 24 | 25 | /** 26 | * The desktop uri 27 | */ 28 | private Uri mOAuthDesktopUri; 29 | 30 | /** 31 | * The logout uri 32 | */ 33 | private Uri mOAuthLogoutUri; 34 | 35 | /** 36 | * The auth token uri 37 | */ 38 | private Uri mOAuthTokenUri; 39 | 40 | /** 41 | * Default Constructor 42 | */ 43 | public MicrosoftOAuthConfig() { 44 | mOAuthAuthorizeUri = Uri.parse(HTTPS_LOGIN_LIVE_COM + "oauth20_authorize.srf"); 45 | mOAuthDesktopUri = Uri.parse(HTTPS_LOGIN_LIVE_COM + "oauth20_desktop.srf"); 46 | mOAuthLogoutUri = Uri.parse(HTTPS_LOGIN_LIVE_COM + "oauth20_logout.srf"); 47 | mOAuthTokenUri = Uri.parse(HTTPS_LOGIN_LIVE_COM + "oauth20_token.srf"); 48 | } 49 | 50 | /** 51 | * Gets an instance of the OneDrive OAuth configuration 52 | * @return The singleton instance 53 | */ 54 | public static MicrosoftOAuthConfig getInstance() { 55 | if (MicrosoftOAuthConfig.sInstance == null) { 56 | MicrosoftOAuthConfig.sInstance = new MicrosoftOAuthConfig(); 57 | } 58 | 59 | return MicrosoftOAuthConfig.sInstance; 60 | } 61 | 62 | @Override 63 | public Uri getAuthorizeUri() { 64 | return mOAuthAuthorizeUri; 65 | } 66 | 67 | @Override 68 | public Uri getDesktopUri() { 69 | return mOAuthDesktopUri; 70 | } 71 | 72 | @Override 73 | public Uri getLogoutUri() { 74 | return mOAuthLogoutUri; 75 | } 76 | 77 | @Override 78 | public Uri getTokenUri() { 79 | return mOAuthTokenUri; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/com/microsoft/services/msa/OAuth.java: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) 2014 Microsoft Corporation 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // ------------------------------------------------------------------------------ 22 | 23 | package com.microsoft.services.msa; 24 | 25 | /** 26 | * OAuth is a non-instantiable utility class that contains types and constants 27 | * for the OAuth protocol. 28 | * 29 | * See the OAuth 2.0 spec 30 | * for more information. 31 | */ 32 | final class OAuth { 33 | 34 | public enum DisplayType { 35 | ANDROID_PHONE, 36 | ANDROID_TABLET 37 | } 38 | 39 | public enum ErrorType { 40 | /** 41 | * Client authentication failed (e.g. unknown client, no 42 | * client authentication included, or unsupported 43 | * authentication method). The authorization server MAY 44 | * return an HTTP 401 (Unauthorized) status code to indicate 45 | * which HTTP authentication schemes are supported. If the 46 | * client attempted to authenticate via the "Authorization" 47 | * request header field, the authorization server MUST 48 | * respond with an HTTP 401 (Unauthorized) status code, and 49 | * include the "WWW-Authenticate" response header field 50 | * matching the authentication scheme used by the client. 51 | */ 52 | INVALID_CLIENT, 53 | 54 | /** 55 | * The provided authorization grant (e.g. authorization 56 | * code, resource owner credentials, client credentials) is 57 | * invalid, expired, revoked, does not match the redirection 58 | * URI used in the authorization request, or was issued to 59 | * another client. 60 | */ 61 | INVALID_GRANT, 62 | 63 | /** 64 | * The request is missing a required parameter, includes an 65 | * unsupported parameter value, repeats a parameter, 66 | * includes multiple credentials, utilizes more than one 67 | * mechanism for authenticating the client, or is otherwise 68 | * malformed. 69 | */ 70 | INVALID_REQUEST, 71 | 72 | /** 73 | * The requested scope is invalid, unknown, malformed, or 74 | * exceeds the scope granted by the resource owner. 75 | */ 76 | INVALID_SCOPE, 77 | 78 | /** 79 | * The authenticated client is not authorized to use this 80 | * authorization grant type. 81 | */ 82 | UNAUTHORIZED_CLIENT, 83 | 84 | /** 85 | * The authorization grant type is not supported by the 86 | * authorization server. 87 | */ 88 | UNSUPPORTED_GRANT_TYPE; 89 | } 90 | 91 | public enum GrantType { 92 | AUTHORIZATION_CODE, 93 | CLIENT_CREDENTIALS, 94 | PASSWORD, 95 | REFRESH_TOKEN; 96 | } 97 | 98 | public enum ResponseType { 99 | CODE, 100 | TOKEN; 101 | } 102 | 103 | public enum TokenType { 104 | BEARER 105 | } 106 | 107 | /** 108 | * Key for the access_token parameter. 109 | * 110 | * See Section 5.1 111 | * of the OAuth 2.0 spec for more information. 112 | */ 113 | public static final String ACCESS_TOKEN = "access_token"; 114 | 115 | /** The app's authentication token. */ 116 | public static final String AUTHENTICATION_TOKEN = "authentication_token"; 117 | 118 | /** The app's client ID. */ 119 | public static final String CLIENT_ID = "client_id"; 120 | 121 | /** Equivalent to the profile that is described in the OAuth 2.0 protocol spec. */ 122 | public static final String CODE = "code"; 123 | 124 | /** 125 | * The display type to be used for the authorization page. Valid values are 126 | * "popup", "touch", "page", or "none". 127 | */ 128 | public static final String DISPLAY = "display"; 129 | 130 | /** 131 | * Key for the error parameter. 132 | * 133 | * error can have the following values: 134 | * invalid_request, unauthorized_client, access_denied, unsupported_response_type, 135 | * invalid_scope, server_error, or temporarily_unavailable. 136 | */ 137 | public static final String ERROR = "error"; 138 | 139 | /** 140 | * Key for the error_description parameter. error_description is described below. 141 | * 142 | * OPTIONAL. A human-readable UTF-8 encoded text providing 143 | * additional information, used to assist the client developer in 144 | * understanding the error that occurred. 145 | */ 146 | public static final String ERROR_DESCRIPTION = "error_description"; 147 | 148 | /** 149 | * Key for the error_uri parameter. error_uri is described below. 150 | * 151 | * OPTIONAL. A URI identifying a human-readable web page with 152 | * information about the error, used to provide the client 153 | * developer with additional information about the error. 154 | */ 155 | public static final String ERROR_URI = "error_uri"; 156 | 157 | /** 158 | * Key for the expires_in parameter. expires_in is described below. 159 | * 160 | * OPTIONAL. The lifetime in seconds of the access token. For 161 | * example, the value "3600" denotes that the access token will 162 | * expire in one hour from the time the response was generated. 163 | */ 164 | public static final String EXPIRES_IN = "expires_in"; 165 | 166 | /** 167 | * Key for the grant_type parameter. grant_type is described below. 168 | * 169 | * grant_type is used in a token request. It can take on the following 170 | * values: authorization_code, password, client_credentials, or refresh_token. 171 | */ 172 | public static final String GRANT_TYPE = "grant_type"; 173 | 174 | /** 175 | * Optional. A market string that determines how the consent user interface 176 | * (UI) is localized. If the value of this parameter is missing or is not 177 | * valid, a market value is determined by using an internal algorithm. 178 | */ 179 | public static final String LOCALE = "locale"; 180 | 181 | /** 182 | * Key for the redirect_uri parameter. 183 | * 184 | * See Section 3.1.2 185 | * of the OAuth 2.0 spec for more information. 186 | */ 187 | public static final String REDIRECT_URI = "redirect_uri"; 188 | 189 | /** 190 | * Key used for the refresh_token parameter. 191 | * 192 | * See Section 5.1 193 | * of the OAuth 2.0 spec for more information. 194 | */ 195 | public static final String REFRESH_TOKEN = "refresh_token"; 196 | 197 | /** 198 | * The type of data to be returned in the response from the authorization 199 | * server. Valid values are "code" or "token". 200 | */ 201 | public static final String RESPONSE_TYPE = "response_type"; 202 | 203 | /** 204 | * Equivalent to the scope parameter that is described in the OAuth 2.0 205 | * protocol spec. 206 | */ 207 | public static final String SCOPE = "scope"; 208 | 209 | /** Delimiter for the scopes field response. */ 210 | public static final String SCOPE_DELIMITER = " "; 211 | 212 | /** 213 | * Equivalent to the state parameter that is described in the OAuth 2.0 214 | * protocol spec. 215 | */ 216 | public static final String STATE = "state"; 217 | 218 | /** 219 | * The login hint for the username to be pre-filled out in the web form 220 | */ 221 | public static final String LOGIN_HINT = "login_hint"; 222 | 223 | /** 224 | * The alternative login hint for the username to be pre-filled out in the web form 225 | */ 226 | public static final String USER_NAME = "username"; 227 | 228 | public static final String THEME = "theme"; 229 | 230 | /** 231 | * Key used for the token_type parameter. 232 | * 233 | * See Section 5.1 234 | * of the OAuth 2.0 spec for more information. 235 | */ 236 | public static final String TOKEN_TYPE = "token_type"; 237 | 238 | /** Private to prevent instantiation */ 239 | private OAuth() { throw new AssertionError(ErrorMessages.NON_INSTANTIABLE_CLASS); } 240 | } 241 | -------------------------------------------------------------------------------- /src/main/java/com/microsoft/services/msa/OAuthConfig.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.services.msa; 2 | 3 | import android.net.Uri; 4 | 5 | /** 6 | * OAuth endpoint configuration. 7 | */ 8 | public interface OAuthConfig { 9 | 10 | /** 11 | * The authorization uri 12 | * @return the value 13 | */ 14 | Uri getAuthorizeUri(); 15 | 16 | /** 17 | * The desktop uri 18 | * @return the value 19 | */ 20 | Uri getDesktopUri(); 21 | 22 | /** 23 | * The logout uri 24 | * @return the value 25 | */ 26 | Uri getLogoutUri(); 27 | 28 | /** 29 | * The auth token uri 30 | * @return the value 31 | */ 32 | Uri getTokenUri(); 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/microsoft/services/msa/OAuthErrorResponse.java: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) 2014 Microsoft Corporation 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // ------------------------------------------------------------------------------ 22 | 23 | package com.microsoft.services.msa; 24 | 25 | import java.util.Locale; 26 | 27 | import org.json.JSONException; 28 | import org.json.JSONObject; 29 | 30 | 31 | /** 32 | * OAuthErrorResponse represents the an Error Response from the OAuth server. 33 | */ 34 | class OAuthErrorResponse implements OAuthResponse { 35 | 36 | /** 37 | * Builder is a helper class to create a OAuthErrorResponse. 38 | * An OAuthResponse must contain an error, but an error_description and 39 | * error_uri are optional 40 | */ 41 | public static class Builder { 42 | private final OAuth.ErrorType error; 43 | private String errorDescription; 44 | private String errorUri; 45 | 46 | public Builder(OAuth.ErrorType error) { 47 | if (error == null) throw new AssertionError(); 48 | 49 | this.error = error; 50 | } 51 | 52 | /** 53 | * @return a new instance of an OAuthErrorResponse containing 54 | * the values called on the builder. 55 | */ 56 | public OAuthErrorResponse build() { 57 | return new OAuthErrorResponse(this); 58 | } 59 | 60 | public Builder errorDescription(String errorDescription) { 61 | this.errorDescription = errorDescription; 62 | return this; 63 | } 64 | 65 | public Builder errorUri(String errorUri) { 66 | this.errorUri = errorUri; 67 | return this; 68 | } 69 | } 70 | 71 | /** 72 | * Static constructor that creates an OAuthErrorResponse from the given OAuth server's 73 | * JSONObject response 74 | * @param response from the OAuth server 75 | * @return A new instance of an OAuthErrorResponse from the given response 76 | * @throws LiveAuthException if there is an JSONException, or the error type cannot be found. 77 | */ 78 | public static OAuthErrorResponse createFromJson(JSONObject response) throws LiveAuthException { 79 | final String errorString; 80 | try { 81 | errorString = response.getString(OAuth.ERROR); 82 | } catch (JSONException e) { 83 | throw new LiveAuthException(ErrorMessages.SERVER_ERROR, e); 84 | } 85 | 86 | final OAuth.ErrorType error; 87 | try { 88 | error = OAuth.ErrorType.valueOf(errorString.toUpperCase()); 89 | } catch (IllegalArgumentException e) { 90 | throw new LiveAuthException(ErrorMessages.SERVER_ERROR, e); 91 | } catch (NullPointerException e) { 92 | throw new LiveAuthException(ErrorMessages.SERVER_ERROR, e); 93 | } 94 | 95 | final Builder builder = new Builder(error); 96 | if (response.has(OAuth.ERROR_DESCRIPTION)) { 97 | final String errorDescription; 98 | try { 99 | errorDescription = response.getString(OAuth.ERROR_DESCRIPTION); 100 | } catch (JSONException e) { 101 | throw new LiveAuthException(ErrorMessages.CLIENT_ERROR, e); 102 | } 103 | builder.errorDescription(errorDescription); 104 | } 105 | 106 | if (response.has(OAuth.ERROR_URI)) { 107 | final String errorUri; 108 | try { 109 | errorUri = response.getString(OAuth.ERROR_URI); 110 | } catch (JSONException e) { 111 | throw new LiveAuthException(ErrorMessages.CLIENT_ERROR, e); 112 | } 113 | builder.errorUri(errorUri); 114 | } 115 | 116 | return builder.build(); 117 | } 118 | 119 | /** 120 | * @param response to check 121 | * @return true if the given JSONObject is a valid OAuth response 122 | */ 123 | public static boolean validOAuthErrorResponse(JSONObject response) { 124 | return response.has(OAuth.ERROR); 125 | } 126 | 127 | /** REQUIRED. */ 128 | private final OAuth.ErrorType error; 129 | 130 | /** 131 | * OPTIONAL. A human-readable UTF-8 encoded text providing 132 | * additional information, used to assist the client developer in 133 | * understanding the error that occurred. 134 | */ 135 | private final String errorDescription; 136 | 137 | /** 138 | * OPTIONAL. A URI identifying a human-readable web page with 139 | * information about the error, used to provide the client 140 | * developer with additional information about the error. 141 | */ 142 | private final String errorUri; 143 | 144 | /** 145 | * OAuthErrorResponse constructor. It is private to enforce 146 | * the use of the Builder. 147 | * 148 | * @param builder to use to construct the object. 149 | */ 150 | private OAuthErrorResponse(Builder builder) { 151 | this.error = builder.error; 152 | this.errorDescription = builder.errorDescription; 153 | this.errorUri = builder.errorUri; 154 | } 155 | 156 | @Override 157 | public void accept(OAuthResponseVisitor visitor) { 158 | visitor.visit(this); 159 | } 160 | 161 | /** 162 | * error is a required field. 163 | * @return the error 164 | */ 165 | public OAuth.ErrorType getError() { 166 | return error; 167 | } 168 | 169 | /** 170 | * error_description is an optional field 171 | * @return error_description 172 | */ 173 | public String getErrorDescription() { 174 | return errorDescription; 175 | } 176 | 177 | /** 178 | * error_uri is an optional field 179 | * @return error_uri 180 | */ 181 | public String getErrorUri() { 182 | return errorUri; 183 | } 184 | 185 | @Override 186 | public String toString() { 187 | return String.format("OAuthErrorResponse [error=%s, errorDescription=%s, errorUri=%s]", 188 | error.toString().toLowerCase(Locale.US), errorDescription, errorUri); 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/main/java/com/microsoft/services/msa/OAuthRequestObserver.java: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) 2014 Microsoft Corporation 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // ------------------------------------------------------------------------------ 22 | 23 | package com.microsoft.services.msa; 24 | 25 | /** 26 | * An observer of an OAuth Request. It will be notified of an Exception or of a Response. 27 | */ 28 | interface OAuthRequestObserver { 29 | /** 30 | * Callback used on an exception. 31 | * 32 | * @param exception 33 | */ 34 | public void onException(LiveAuthException exception); 35 | 36 | /** 37 | * Callback used on a response. 38 | * 39 | * @param response 40 | */ 41 | public void onResponse(OAuthResponse response); 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/microsoft/services/msa/OAuthResponse.java: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) 2014 Microsoft Corporation 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // ------------------------------------------------------------------------------ 22 | 23 | package com.microsoft.services.msa; 24 | 25 | /** 26 | * OAuthRespresent a response from an OAuth server. 27 | * Known implementors are OAuthSuccessfulResponse and OAuthErrorResponse. 28 | * Different OAuthResponses can be determined by using the OAuthResponseVisitor. 29 | */ 30 | interface OAuthResponse { 31 | 32 | /** 33 | * Calls visit() on the visitor. 34 | * This method is used to determine which OAuthResponse is being returned 35 | * without using instance of. 36 | * 37 | * @param visitor to visit the given OAuthResponse 38 | */ 39 | public void accept(OAuthResponseVisitor visitor); 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/microsoft/services/msa/OAuthResponseVisitor.java: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) 2014 Microsoft Corporation 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // ------------------------------------------------------------------------------ 22 | 23 | package com.microsoft.services.msa; 24 | 25 | /** 26 | * OAuthResponseVisitor is used to visit various OAuthResponse. 27 | */ 28 | interface OAuthResponseVisitor { 29 | 30 | /** 31 | * Called when an OAuthSuccessfulResponse is visited. 32 | * 33 | * @param response being visited 34 | */ 35 | public void visit(OAuthSuccessfulResponse response); 36 | 37 | /** 38 | * Called when an OAuthErrorResponse is being visited. 39 | * 40 | * @param response being visited 41 | */ 42 | public void visit(OAuthErrorResponse response); 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/microsoft/services/msa/OAuthSuccessfulResponse.java: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) 2014 Microsoft Corporation 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // ------------------------------------------------------------------------------ 22 | 23 | package com.microsoft.services.msa; 24 | 25 | import java.util.Map; 26 | 27 | import org.json.JSONException; 28 | import org.json.JSONObject; 29 | 30 | import android.text.TextUtils; 31 | 32 | 33 | /** 34 | * OAuthSuccessfulResponse represents a successful response form an OAuth server. 35 | */ 36 | class OAuthSuccessfulResponse implements OAuthResponse { 37 | 38 | /** 39 | * Builder is a utility class that is used to build a new OAuthSuccessfulResponse. 40 | * It must be constructed with the required fields, and can add on the optional ones. 41 | */ 42 | public static class Builder { 43 | private final String accessToken; 44 | private String authenticationToken; 45 | private int expiresIn = UNINITIALIZED; 46 | private String refreshToken; 47 | private String scope; 48 | private final OAuth.TokenType tokenType; 49 | 50 | public Builder(String accessToken, OAuth.TokenType tokenType) { 51 | if (accessToken == null) throw new AssertionError(); 52 | if (TextUtils.isEmpty(accessToken)) throw new AssertionError(); 53 | if (tokenType == null) throw new AssertionError(); 54 | 55 | this.accessToken = accessToken; 56 | this.tokenType = tokenType; 57 | } 58 | 59 | public Builder authenticationToken(String authenticationToken) { 60 | this.authenticationToken = authenticationToken; 61 | return this; 62 | } 63 | 64 | /** 65 | * @return a new instance of an OAuthSuccessfulResponse with the given 66 | * parameters passed into the builder. 67 | */ 68 | public OAuthSuccessfulResponse build() { 69 | return new OAuthSuccessfulResponse(this); 70 | } 71 | 72 | public Builder expiresIn(int expiresIn) { 73 | this.expiresIn = expiresIn; 74 | return this; 75 | } 76 | 77 | public Builder refreshToken(String refreshToken) { 78 | this.refreshToken = refreshToken; 79 | return this; 80 | } 81 | 82 | public Builder scope(String scope) { 83 | this.scope = scope; 84 | return this; 85 | } 86 | } 87 | 88 | /** Used to declare expiresIn uninitialized */ 89 | private static final int UNINITIALIZED = -1; 90 | 91 | public static OAuthSuccessfulResponse createFromFragment( 92 | Map fragmentParameters) throws LiveAuthException { 93 | String accessToken = fragmentParameters.get(OAuth.ACCESS_TOKEN); 94 | String tokenTypeString = fragmentParameters.get(OAuth.TOKEN_TYPE); 95 | 96 | // must have accessToken and tokenTypeString to be a valid OAuthSuccessfulResponse 97 | if (accessToken == null) throw new AssertionError(); 98 | if (tokenTypeString == null) throw new AssertionError(); 99 | 100 | OAuth.TokenType tokenType; 101 | try { 102 | tokenType = OAuth.TokenType.valueOf(tokenTypeString.toUpperCase()); 103 | } catch (IllegalArgumentException e) { 104 | throw new LiveAuthException(ErrorMessages.SERVER_ERROR, e); 105 | } 106 | 107 | Builder builder = 108 | new Builder(accessToken, tokenType); 109 | 110 | String authenticationToken = fragmentParameters.get(OAuth.AUTHENTICATION_TOKEN); 111 | if (authenticationToken != null) { 112 | builder.authenticationToken(authenticationToken); 113 | } 114 | 115 | String expiresInString = fragmentParameters.get(OAuth.EXPIRES_IN); 116 | if (expiresInString != null) { 117 | final int expiresIn; 118 | try { 119 | expiresIn = Integer.parseInt(expiresInString); 120 | } catch (final NumberFormatException e) { 121 | throw new LiveAuthException(ErrorMessages.SERVER_ERROR, e); 122 | } 123 | 124 | builder.expiresIn(expiresIn); 125 | } 126 | 127 | String scope = fragmentParameters.get(OAuth.SCOPE); 128 | if (scope != null) { 129 | builder.scope(scope); 130 | } 131 | 132 | return builder.build(); 133 | } 134 | 135 | /** 136 | * Static constructor used to create a new OAuthSuccessfulResponse from an 137 | * OAuth server's JSON response. 138 | * 139 | * @param response from an OAuth server that is used to create the object. 140 | * @return a new instance of OAuthSuccessfulResponse that is created from the given JSONObject 141 | * @throws LiveAuthException if there is a JSONException or the token_type is unknown. 142 | */ 143 | public static OAuthSuccessfulResponse createFromJson(JSONObject response) 144 | throws LiveAuthException { 145 | if (!validOAuthSuccessfulResponse(response)) throw new AssertionError(); 146 | 147 | final String accessToken; 148 | try { 149 | accessToken = response.getString(OAuth.ACCESS_TOKEN); 150 | } catch (final JSONException e) { 151 | throw new LiveAuthException(ErrorMessages.SERVER_ERROR, e); 152 | } 153 | 154 | final String tokenTypeString; 155 | try { 156 | tokenTypeString = response.getString(OAuth.TOKEN_TYPE); 157 | } catch (final JSONException e) { 158 | throw new LiveAuthException(ErrorMessages.SERVER_ERROR, e); 159 | } 160 | 161 | final OAuth.TokenType tokenType; 162 | try { 163 | tokenType = OAuth.TokenType.valueOf(tokenTypeString.toUpperCase()); 164 | } catch (final IllegalArgumentException e) { 165 | throw new LiveAuthException(ErrorMessages.SERVER_ERROR, e); 166 | } catch (final NullPointerException e) { 167 | throw new LiveAuthException(ErrorMessages.SERVER_ERROR, e); 168 | } 169 | 170 | final Builder builder = new Builder(accessToken, tokenType); 171 | 172 | if (response.has(OAuth.AUTHENTICATION_TOKEN)) { 173 | final String authenticationToken; 174 | try { 175 | authenticationToken = response.getString(OAuth.AUTHENTICATION_TOKEN); 176 | } catch (final JSONException e) { 177 | throw new LiveAuthException(ErrorMessages.CLIENT_ERROR, e); 178 | } 179 | builder.authenticationToken(authenticationToken); 180 | } 181 | 182 | if (response.has(OAuth.REFRESH_TOKEN)) { 183 | final String refreshToken; 184 | try { 185 | refreshToken = response.getString(OAuth.REFRESH_TOKEN); 186 | } catch (final JSONException e) { 187 | throw new LiveAuthException(ErrorMessages.CLIENT_ERROR, e); 188 | } 189 | builder.refreshToken(refreshToken); 190 | } 191 | 192 | if (response.has(OAuth.EXPIRES_IN)) { 193 | final int expiresIn; 194 | try { 195 | expiresIn = response.getInt(OAuth.EXPIRES_IN); 196 | } catch (final JSONException e) { 197 | throw new LiveAuthException(ErrorMessages.CLIENT_ERROR, e); 198 | } 199 | builder.expiresIn(expiresIn); 200 | } 201 | 202 | if (response.has(OAuth.SCOPE)) { 203 | final String scope; 204 | try { 205 | scope = response.getString(OAuth.SCOPE); 206 | } catch (final JSONException e) { 207 | throw new LiveAuthException(ErrorMessages.CLIENT_ERROR, e); 208 | } 209 | builder.scope(scope); 210 | } 211 | 212 | return builder.build(); 213 | } 214 | 215 | /** 216 | * @param response 217 | * @return true if the given JSONObject has the required fields to construct an 218 | * OAuthSuccessfulResponse (i.e., has access_token and token_type) 219 | */ 220 | public static boolean validOAuthSuccessfulResponse(JSONObject response) { 221 | return response.has(OAuth.ACCESS_TOKEN) && 222 | response.has(OAuth.TOKEN_TYPE); 223 | } 224 | 225 | /** REQUIRED. The access token issued by the authorization server. */ 226 | private final String accessToken; 227 | 228 | private final String authenticationToken; 229 | 230 | /** 231 | * OPTIONAL. The lifetime in seconds of the access token. For 232 | * example, the value "3600" denotes that the access token will 233 | * expire in one hour from the time the response was generated. 234 | */ 235 | private final int expiresIn; 236 | 237 | /** 238 | * OPTIONAL. The refresh token which can be used to obtain new 239 | * access tokens using the same authorization grant. 240 | */ 241 | private final String refreshToken; 242 | 243 | /** OPTIONAL. */ 244 | private final String scope; 245 | 246 | /** REQUIRED. */ 247 | private final OAuth.TokenType tokenType; 248 | 249 | /** 250 | * Private constructor to enforce the user of the builder. 251 | * @param builder to use to construct the object from. 252 | */ 253 | private OAuthSuccessfulResponse(Builder builder) { 254 | this.accessToken = builder.accessToken; 255 | this.authenticationToken = builder.authenticationToken; 256 | this.tokenType = builder.tokenType; 257 | this.refreshToken = builder.refreshToken; 258 | this.expiresIn = builder.expiresIn; 259 | this.scope = builder.scope; 260 | } 261 | 262 | @Override 263 | public void accept(OAuthResponseVisitor visitor) { 264 | visitor.visit(this); 265 | } 266 | 267 | public String getAccessToken() { 268 | return this.accessToken; 269 | } 270 | 271 | public String getAuthenticationToken() { 272 | return this.authenticationToken; 273 | } 274 | 275 | public int getExpiresIn() { 276 | return this.expiresIn; 277 | } 278 | 279 | public String getRefreshToken() { 280 | return this.refreshToken; 281 | } 282 | 283 | public String getScope() { 284 | return this.scope; 285 | } 286 | 287 | public OAuth.TokenType getTokenType() { 288 | return this.tokenType; 289 | } 290 | 291 | public boolean hasAuthenticationToken() { 292 | return this.authenticationToken != null && !TextUtils.isEmpty(this.authenticationToken); 293 | } 294 | 295 | public boolean hasExpiresIn() { 296 | return this.expiresIn != UNINITIALIZED; 297 | } 298 | 299 | public boolean hasRefreshToken() { 300 | return this.refreshToken != null && !TextUtils.isEmpty(this.refreshToken); 301 | } 302 | 303 | public boolean hasScope() { 304 | return this.scope != null && !TextUtils.isEmpty(this.scope); 305 | } 306 | 307 | @Override 308 | public String toString() { 309 | return String.format("OAuthSuccessfulResponse [accessToken=%s, authenticationToken=%s, tokenType=%s, refreshToken=%s, expiresIn=%s, scope=%s]", 310 | this.accessToken, 311 | this.authenticationToken, 312 | this.tokenType, 313 | this.refreshToken, 314 | this.expiresIn, 315 | this.scope); 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /src/main/java/com/microsoft/services/msa/ObservableOAuthRequest.java: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) 2014 Microsoft Corporation 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // ------------------------------------------------------------------------------ 22 | 23 | package com.microsoft.services.msa; 24 | 25 | /** 26 | * An OAuth Request that can be observed, by adding observers that will be notified on any 27 | * exception or response. 28 | */ 29 | interface ObservableOAuthRequest { 30 | /** 31 | * Adds an observer to observe the OAuth request 32 | * 33 | * @param observer to add 34 | */ 35 | public void addObserver(OAuthRequestObserver observer); 36 | 37 | /** 38 | * Removes an observer that is observing the OAuth request 39 | * 40 | * @param observer to remove 41 | * @return true if the observer was removed. 42 | */ 43 | public boolean removeObserver(OAuthRequestObserver observer); 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/microsoft/services/msa/OverwriteOption.java: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) 2014 Microsoft Corporation 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // ------------------------------------------------------------------------------ 22 | 23 | package com.microsoft.services.msa; 24 | 25 | /** 26 | * Enum that specifies what to do during a naming conflict during an upload. 27 | */ 28 | public enum OverwriteOption { 29 | 30 | /** Overwrite the existing file. */ 31 | Overwrite { 32 | @Override 33 | protected String overwriteQueryParamValue() { 34 | return "true"; 35 | } 36 | }, 37 | 38 | /** Do Not Overwrite the existing file and cancel the upload. */ 39 | DoNotOverwrite { 40 | @Override 41 | protected String overwriteQueryParamValue() { 42 | return "false"; 43 | } 44 | }, 45 | 46 | /** Rename the current file to avoid a name conflict. */ 47 | Rename { 48 | @Override 49 | protected String overwriteQueryParamValue() { 50 | return "choosenewname"; 51 | } 52 | }; 53 | 54 | /** 55 | * Leaves any existing overwrite query parameter on appends this overwrite 56 | * to the given UriBuilder. 57 | */ 58 | void appendQueryParameterOnTo(UriBuilder uri) { 59 | uri.appendQueryParameter(QueryParameters.OVERWRITE, this.overwriteQueryParamValue()); 60 | } 61 | 62 | abstract protected String overwriteQueryParamValue(); 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/microsoft/services/msa/PreferencesConstants.java: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) 2014 Microsoft Corporation 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // ------------------------------------------------------------------------------ 22 | 23 | package com.microsoft.services.msa; 24 | 25 | /** 26 | * Static class that holds constants used by an application's preferences. 27 | */ 28 | final class PreferencesConstants { 29 | public static final String COOKIES_KEY = "cookies"; 30 | 31 | /** Name of the preference file */ 32 | public static final String FILE_NAME = "com.microsoft.live"; 33 | 34 | public static final String REFRESH_TOKEN_KEY = "refresh_token"; 35 | public static final String COOKIE_DELIMITER = ","; 36 | 37 | private PreferencesConstants() { throw new AssertionError(); } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/microsoft/services/msa/QueryParameters.java: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) 2014 Microsoft Corporation 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // ------------------------------------------------------------------------------ 22 | 23 | package com.microsoft.services.msa; 24 | 25 | /** 26 | * QueryParameters is a non-instantiable utility class that holds query parameter constants 27 | * used by the API service. 28 | */ 29 | final class QueryParameters { 30 | 31 | public static final String PRETTY = "pretty"; 32 | public static final String CALLBACK = "callback"; 33 | public static final String SUPPRESS_REDIRECTS = "suppress_redirects"; 34 | public static final String SUPPRESS_RESPONSE_CODES = "suppress_response_codes"; 35 | public static final String METHOD = "method"; 36 | public static final String OVERWRITE = "overwrite"; 37 | public static final String RETURN_SSL_RESOURCES = "return_ssl_resources"; 38 | 39 | /** Private to present instantiation. */ 40 | private QueryParameters() { 41 | throw new AssertionError(ErrorMessages.NON_INSTANTIABLE_CLASS); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/microsoft/services/msa/RefreshAccessTokenRequest.java: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) 2014 Microsoft Corporation 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // ------------------------------------------------------------------------------ 22 | 23 | package com.microsoft.services.msa; 24 | 25 | import java.util.List; 26 | 27 | import org.apache.http.NameValuePair; 28 | import org.apache.http.client.HttpClient; 29 | import org.apache.http.message.BasicNameValuePair; 30 | 31 | import android.text.TextUtils; 32 | 33 | 34 | /** 35 | * RefreshAccessTokenRequest performs a refresh access token request. Most of the work 36 | * is done by the parent class, TokenRequest. This class adds in the required body parameters via 37 | * TokenRequest's hook method, constructBody(). 38 | */ 39 | class RefreshAccessTokenRequest extends TokenRequest { 40 | 41 | /** REQUIRED. Value MUST be set to "refresh_token". */ 42 | private final OAuth.GrantType grantType = OAuth.GrantType.REFRESH_TOKEN; 43 | 44 | /** REQUIRED. The refresh token issued to the client. */ 45 | private final String refreshToken; 46 | 47 | private final String scope; 48 | 49 | public RefreshAccessTokenRequest(final HttpClient client, 50 | final String clientId, 51 | final String refreshToken, 52 | final String scope, 53 | final OAuthConfig oAuthConfig) { 54 | super(client, clientId, oAuthConfig); 55 | 56 | if (refreshToken == null) throw new AssertionError(); 57 | if (TextUtils.isEmpty(refreshToken)) throw new AssertionError(); 58 | if (scope == null) throw new AssertionError(); 59 | if (TextUtils.isEmpty(scope)) throw new AssertionError(); 60 | 61 | this.refreshToken = refreshToken; 62 | this.scope = scope; 63 | } 64 | 65 | @Override 66 | protected void constructBody(List body) { 67 | body.add(new BasicNameValuePair(OAuth.REFRESH_TOKEN, this.refreshToken)); 68 | body.add(new BasicNameValuePair(OAuth.SCOPE, this.scope)); 69 | body.add(new BasicNameValuePair(OAuth.GRANT_TYPE, this.grantType.toString())); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/com/microsoft/services/msa/ScreenSize.java: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) 2014 Microsoft Corporation 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // ------------------------------------------------------------------------------ 22 | 23 | package com.microsoft.services.msa; 24 | 25 | import android.app.Activity; 26 | import android.content.res.Configuration; 27 | import android.util.Log; 28 | 29 | import com.microsoft.services.msa.DeviceType; 30 | 31 | /** 32 | * The ScreenSize is used to determine the DeviceType. 33 | * Small and Normal ScreenSizes are Phones. 34 | * Large and XLarge are Tablets. 35 | */ 36 | enum ScreenSize { 37 | SMALL { 38 | @Override 39 | public DeviceType getDeviceType() { 40 | return DeviceType.PHONE; 41 | } 42 | }, 43 | NORMAL { 44 | @Override 45 | public DeviceType getDeviceType() { 46 | return DeviceType.PHONE; 47 | } 48 | 49 | }, 50 | LARGE { 51 | @Override 52 | public DeviceType getDeviceType() { 53 | return DeviceType.TABLET; 54 | } 55 | }, 56 | XLARGE { 57 | @Override 58 | public DeviceType getDeviceType() { 59 | return DeviceType.TABLET; 60 | } 61 | }; 62 | 63 | public abstract DeviceType getDeviceType(); 64 | 65 | /** 66 | * Configuration.SCREENLAYOUT_SIZE_XLARGE was not provided in API level 9. 67 | * However, its value of 4 does show up. 68 | */ 69 | private static final int SCREENLAYOUT_SIZE_XLARGE = 4; 70 | 71 | public static ScreenSize determineScreenSize(Activity activity) { 72 | int screenLayout = activity.getResources().getConfiguration().screenLayout; 73 | int screenLayoutMasked = screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK; 74 | switch (screenLayoutMasked) { 75 | case Configuration.SCREENLAYOUT_SIZE_SMALL: 76 | return SMALL; 77 | case Configuration.SCREENLAYOUT_SIZE_NORMAL: 78 | return NORMAL; 79 | case Configuration.SCREENLAYOUT_SIZE_LARGE: 80 | return LARGE; 81 | case SCREENLAYOUT_SIZE_XLARGE: 82 | return XLARGE; 83 | default: 84 | // If we cannot determine the ScreenSize, we'll guess and say it's normal. 85 | Log.d( 86 | "Live SDK ScreenSize", 87 | "Unable to determine ScreenSize. A Normal ScreenSize will be returned."); 88 | return NORMAL; 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/com/microsoft/services/msa/TokenRequest.java: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) 2014 Microsoft Corporation 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // ------------------------------------------------------------------------------ 22 | 23 | package com.microsoft.services.msa; 24 | 25 | import java.io.IOException; 26 | import java.io.UnsupportedEncodingException; 27 | import java.util.ArrayList; 28 | import java.util.List; 29 | 30 | import org.apache.http.HttpEntity; 31 | import org.apache.http.HttpResponse; 32 | import org.apache.http.NameValuePair; 33 | import org.apache.http.client.ClientProtocolException; 34 | import org.apache.http.client.HttpClient; 35 | import org.apache.http.client.entity.UrlEncodedFormEntity; 36 | import org.apache.http.client.methods.HttpPost; 37 | import org.apache.http.client.utils.URLEncodedUtils; 38 | import org.apache.http.message.BasicNameValuePair; 39 | import org.apache.http.protocol.HTTP; 40 | import org.apache.http.util.EntityUtils; 41 | import org.json.JSONException; 42 | import org.json.JSONObject; 43 | 44 | import android.net.Uri; 45 | import android.text.TextUtils; 46 | 47 | /** 48 | * Abstract class that represents an OAuth token request. 49 | * Known subclasses include AccessTokenRequest and RefreshAccessTokenRequest 50 | */ 51 | abstract class TokenRequest { 52 | 53 | private static final String CONTENT_TYPE = 54 | URLEncodedUtils.CONTENT_TYPE + ";charset=" + HTTP.UTF_8; 55 | 56 | protected final HttpClient client; 57 | protected final String clientId; 58 | protected final OAuthConfig mOAuthConfig; 59 | 60 | /** 61 | * Constructs a new TokenRequest instance and initializes its parameters. 62 | * 63 | * @param client the HttpClient to make HTTP requests on 64 | * @param clientId the client_id of the calling application 65 | */ 66 | public TokenRequest(HttpClient client, String clientId, final OAuthConfig oAuthConfig) { 67 | if (client == null) throw new AssertionError(); 68 | if (clientId == null) throw new AssertionError(); 69 | if (TextUtils.isEmpty(clientId)) throw new AssertionError(); 70 | 71 | this.client = client; 72 | this.clientId = clientId; 73 | this.mOAuthConfig = oAuthConfig; 74 | } 75 | 76 | /** 77 | * Performs the Token Request and returns the OAuth server's response. 78 | * 79 | * @return The OAuthResponse from the server 80 | * @throws LiveAuthException if there is any exception while executing the request 81 | * (e.g., IOException, JSONException) 82 | */ 83 | public OAuthResponse execute() throws LiveAuthException { 84 | final Uri requestUri = mOAuthConfig.getTokenUri(); 85 | 86 | final HttpPost request = new HttpPost(requestUri.toString()); 87 | 88 | final List body = new ArrayList(); 89 | body.add(new BasicNameValuePair(OAuth.CLIENT_ID, this.clientId)); 90 | 91 | // constructBody allows subclasses to add to body 92 | this.constructBody(body); 93 | 94 | try { 95 | final UrlEncodedFormEntity entity = new UrlEncodedFormEntity(body, HTTP.UTF_8); 96 | entity.setContentType(CONTENT_TYPE); 97 | request.setEntity(entity); 98 | } catch (UnsupportedEncodingException e) { 99 | throw new LiveAuthException(ErrorMessages.CLIENT_ERROR, e); 100 | } 101 | 102 | final HttpResponse response; 103 | try { 104 | response = this.client.execute(request); 105 | } catch (ClientProtocolException e) { 106 | throw new LiveAuthException(ErrorMessages.SERVER_ERROR, e); 107 | } catch (IOException e) { 108 | throw new LiveAuthException(ErrorMessages.SERVER_ERROR, e); 109 | } 110 | 111 | final HttpEntity entity = response.getEntity(); 112 | final String stringResponse; 113 | try { 114 | stringResponse = EntityUtils.toString(entity); 115 | } catch (IOException e) { 116 | throw new LiveAuthException(ErrorMessages.SERVER_ERROR, e); 117 | } 118 | 119 | final JSONObject jsonResponse; 120 | try { 121 | jsonResponse = new JSONObject(stringResponse); 122 | } catch (JSONException e) { 123 | throw new LiveAuthException(ErrorMessages.SERVER_ERROR, e); 124 | } 125 | 126 | if (OAuthErrorResponse.validOAuthErrorResponse(jsonResponse)) { 127 | return OAuthErrorResponse.createFromJson(jsonResponse); 128 | } else if (OAuthSuccessfulResponse.validOAuthSuccessfulResponse(jsonResponse)) { 129 | return OAuthSuccessfulResponse.createFromJson(jsonResponse); 130 | } else { 131 | throw new LiveAuthException(ErrorMessages.SERVER_ERROR); 132 | } 133 | } 134 | 135 | /** 136 | * This method gives a hook in the execute process, and allows subclasses 137 | * to add to the HttpRequest's body. 138 | * NOTE: The content type has already been added 139 | * 140 | * @param body of NameValuePairs to add to 141 | */ 142 | protected abstract void constructBody(List body); 143 | } 144 | -------------------------------------------------------------------------------- /src/main/java/com/microsoft/services/msa/TokenRequestAsync.java: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) 2014 Microsoft Corporation 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // ------------------------------------------------------------------------------ 22 | 23 | package com.microsoft.services.msa; 24 | 25 | import android.os.AsyncTask; 26 | 27 | /** 28 | * TokenRequestAsync performs an async token request. It takes in a TokenRequest, 29 | * executes it, checks the OAuthResponse, and then calls the given listener. 30 | */ 31 | class TokenRequestAsync extends AsyncTask implements ObservableOAuthRequest { 32 | 33 | private final DefaultObservableOAuthRequest observerable; 34 | 35 | /** Not null if there was an exception */ 36 | private LiveAuthException exception; 37 | 38 | /** Not null if there was a response */ 39 | private OAuthResponse response; 40 | 41 | private final TokenRequest request; 42 | 43 | /** 44 | * Constructs a new TokenRequestAsync and initializes its member variables 45 | * 46 | * @param request to perform 47 | */ 48 | public TokenRequestAsync(TokenRequest request) { 49 | if (request == null) throw new AssertionError(); 50 | 51 | this.observerable = new DefaultObservableOAuthRequest(); 52 | this.request = request; 53 | } 54 | 55 | @Override 56 | public void addObserver(OAuthRequestObserver observer) { 57 | this.observerable.addObserver(observer); 58 | } 59 | 60 | @Override 61 | public boolean removeObserver(OAuthRequestObserver observer) { 62 | return this.observerable.removeObserver(observer); 63 | } 64 | 65 | @Override 66 | protected Void doInBackground(Void... params) { 67 | try { 68 | this.response = this.request.execute(); 69 | } catch (LiveAuthException e) { 70 | this.exception = e; 71 | } 72 | 73 | return null; 74 | } 75 | 76 | @Override 77 | protected void onPostExecute(Void result) { 78 | super.onPostExecute(result); 79 | 80 | if (this.response != null) { 81 | this.observerable.notifyObservers(this.response); 82 | } else if (this.exception != null) { 83 | this.observerable.notifyObservers(this.exception); 84 | } else { 85 | final LiveAuthException exception = new LiveAuthException(ErrorMessages.CLIENT_ERROR); 86 | this.observerable.notifyObservers(exception); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/com/microsoft/services/msa/UriBuilder.java: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // Copyright (c) 2014 Microsoft Corporation 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // ------------------------------------------------------------------------------ 22 | 23 | package com.microsoft.services.msa; 24 | 25 | import java.util.Iterator; 26 | import java.util.LinkedList; 27 | 28 | import android.net.Uri; 29 | import android.text.TextUtils; 30 | import android.util.Log; 31 | 32 | /** 33 | * Class for building URIs. The most useful benefit of this class is its query parameter 34 | * management. It stores all the query parameters in a LinkedList, so parameters can 35 | * be looked up, removed, and added easily. 36 | */ 37 | class UriBuilder { 38 | 39 | public static class QueryParameter { 40 | private final String key; 41 | private final String value; 42 | 43 | /** 44 | * Constructs a query parameter with no value (e.g., download). 45 | * 46 | * @param key 47 | */ 48 | public QueryParameter(String key) { 49 | if (key == null) throw new AssertionError(); 50 | 51 | this.key = key; 52 | this.value = null; 53 | } 54 | 55 | public QueryParameter(String key, String value) { 56 | if (key == null) throw new AssertionError(); 57 | if (value == null) throw new AssertionError(); 58 | 59 | this.key = key; 60 | this.value = value; 61 | } 62 | 63 | public String getKey() { 64 | return this.key; 65 | } 66 | 67 | public String getValue() { 68 | return this.value; 69 | } 70 | 71 | public boolean hasValue() { 72 | return this.value != null; 73 | } 74 | 75 | @Override 76 | public String toString() { 77 | if (this.hasValue()) { 78 | return this.key + "=" + this.value; 79 | } 80 | 81 | return this.key; 82 | } 83 | } 84 | 85 | private static final String EQUAL = "="; 86 | private static final String AMPERSAND = "&"; 87 | private static final char FORWARD_SLASH = '/'; 88 | 89 | private String scheme; 90 | private String host; 91 | private StringBuilder path; 92 | 93 | private final LinkedList queryParameters; 94 | 95 | /** 96 | * Constructs a new UriBuilder from the given Uri. 97 | * 98 | * @return a new Uri Builder based off the given Uri. 99 | */ 100 | public static UriBuilder newInstance(Uri uri) { 101 | return new UriBuilder().scheme(uri.getScheme()) 102 | .host(uri.getHost()) 103 | .path(uri.getPath()) 104 | .query(uri.getQuery()); 105 | } 106 | 107 | public UriBuilder() { 108 | this.queryParameters = new LinkedList(); 109 | } 110 | 111 | /** 112 | * Appends a new query parameter to the UriBuilder's query string. 113 | * 114 | * (e.g., appendQueryParameter("k1", "v1") when UriBuilder's query string is 115 | * k2=v2&k3=v3 results in k2=v2&k3=v3&k1=v1). 116 | * 117 | * @param key Key of the new query parameter. 118 | * @param value Value of the new query parameter. 119 | * @return this UriBuilder object. Useful for chaining. 120 | */ 121 | public UriBuilder appendQueryParameter(String key, String value) { 122 | if (key == null) throw new AssertionError(); 123 | if (value == null) throw new AssertionError(); 124 | 125 | this.queryParameters.add(new QueryParameter(key, value)); 126 | 127 | return this; 128 | } 129 | 130 | /** 131 | * Appends the given query string on to the existing UriBuilder's query parameters. 132 | * 133 | * (e.g., UriBuilder's queryString k1=v1&k2=v2 and given queryString k3=v3&k4=v4, results in 134 | * k1=v1&k2=v2&k3=v3&k4=v4). 135 | * 136 | * @param queryString Key-Value pairs separated by & and = (e.g., k1=v1&k2=v2&k3=k3). 137 | * @return this UriBuilder object. Useful for chaining. 138 | */ 139 | public UriBuilder appendQueryString(String queryString) { 140 | if (queryString == null) { 141 | return this; 142 | } 143 | 144 | String[] pairs = TextUtils.split(queryString, UriBuilder.AMPERSAND); 145 | for(String pair : pairs) { 146 | String[] splitPair = TextUtils.split(pair, UriBuilder.EQUAL); 147 | if (splitPair.length == 2) { 148 | String key = splitPair[0]; 149 | String value = splitPair[1]; 150 | 151 | this.queryParameters.add(new QueryParameter(key, value)); 152 | } else if (splitPair.length == 1){ 153 | String key = splitPair[0]; 154 | 155 | this.queryParameters.add(new QueryParameter(key)); 156 | } else { 157 | Log.w("live.auth.UriBuilder", "Invalid query parameter: " + pair); 158 | } 159 | } 160 | 161 | return this; 162 | } 163 | 164 | /** 165 | * Appends the given path to the UriBuilder's current path. 166 | * 167 | * @param path The path to append onto this UriBuilder's path. 168 | * @return this UriBuilder object. Useful for chaining. 169 | */ 170 | public UriBuilder appendToPath(String path) { 171 | if (path == null) throw new AssertionError(); 172 | 173 | if (this.path == null) { 174 | this.path = new StringBuilder(path); 175 | } else { 176 | boolean endsWithSlash = TextUtils.isEmpty(this.path) ? false : 177 | this.path.charAt(this.path.length() - 1) == UriBuilder.FORWARD_SLASH; 178 | boolean pathIsEmpty = TextUtils.isEmpty(path); 179 | boolean beginsWithSlash = 180 | pathIsEmpty ? false : path.charAt(0) == UriBuilder.FORWARD_SLASH; 181 | 182 | if (endsWithSlash && beginsWithSlash) { 183 | if (path.length() > 1) { 184 | this.path.append(path.substring(1)); 185 | 186 | } 187 | } else if (!endsWithSlash && !beginsWithSlash) { 188 | if (!pathIsEmpty) { 189 | this.path.append(UriBuilder.FORWARD_SLASH).append(path); 190 | } 191 | } else { 192 | this.path.append(path); 193 | } 194 | } 195 | 196 | return this; 197 | } 198 | 199 | /** 200 | * Builds the Uri by converting into a android.net.Uri object. 201 | * 202 | * @return a new android.net.Uri defined by what was given to the builder. 203 | */ 204 | public Uri build() { 205 | return new Uri.Builder().scheme(this.scheme) 206 | .authority(this.host) 207 | .path(this.path == null ? "" : this.path.toString()) 208 | .encodedQuery(TextUtils.join("&", this.queryParameters)) 209 | .build(); 210 | } 211 | 212 | /** 213 | * Sets the host part of the Uri. 214 | * 215 | * @return this UriBuilder object. Useful for chaining. 216 | */ 217 | public UriBuilder host(String host) { 218 | if (host == null) throw new AssertionError(); 219 | this.host = host; 220 | 221 | return this; 222 | } 223 | 224 | /** 225 | * Sets the path and removes any previously existing path. 226 | * 227 | * @param path The path to set on this UriBuilder. 228 | * @return this UriBuilder object. Useful for chaining. 229 | */ 230 | public UriBuilder path(String path) { 231 | if (path == null) throw new AssertionError(); 232 | this.path = new StringBuilder(path); 233 | 234 | return this; 235 | } 236 | 237 | /** 238 | * Takes a query string and puts it in the Uri Builder's query string removing 239 | * any existing query parameters. 240 | * 241 | * @param queryString Key-Value pairs separated by & and = (e.g., k1=v1&k2=v2&k3=k3). 242 | * @return this UriBuilder object. Useful for chaining. 243 | */ 244 | public UriBuilder query(String queryString) { 245 | this.queryParameters.clear(); 246 | 247 | return this.appendQueryString(queryString); 248 | } 249 | 250 | /** 251 | * Removes all query parameters from the UriBuilder that has the given key. 252 | * 253 | * (e.g., removeQueryParametersWithKey("k1") when UriBuilder's query string of k1=v1&k2=v2&k1=v3 254 | * results in k2=v2). 255 | * 256 | * @param key Query parameter's key to remove 257 | * @return this UriBuilder object. Useful for chaining. 258 | */ 259 | public UriBuilder removeQueryParametersWithKey(String key) { 260 | // There could be multiple query parameters with this key and 261 | // we want to remove all of them. 262 | Iterator it = this.queryParameters.iterator(); 263 | 264 | while (it.hasNext()) { 265 | QueryParameter qp = it.next(); 266 | if (qp.getKey().equals(key)) { 267 | it.remove(); 268 | } 269 | } 270 | 271 | return this; 272 | } 273 | 274 | /** 275 | * Sets the scheme part of the Uri. 276 | * 277 | * @return this UriBuilder object. Useful for chaining. 278 | */ 279 | public UriBuilder scheme(String scheme) { 280 | if (scheme == null) throw new AssertionError(); 281 | this.scheme = scheme; 282 | 283 | return this; 284 | } 285 | 286 | /** 287 | * Returns the URI in string format (e.g., http://foo.com/bar?k1=v2). 288 | */ 289 | @Override 290 | public String toString() { 291 | return this.build().toString(); 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | msa-auth 3 | 4 | --------------------------------------------------------------------------------