├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── README.md ├── codecov.yml ├── install.sh ├── pom.xml ├── src ├── main │ └── java │ │ └── com │ │ └── authy │ │ ├── AuthyApiClient.java │ │ ├── AuthyException.java │ │ ├── AuthyUtil.java │ │ ├── OneTouchException.java │ │ └── api │ │ ├── ApprovalRequestParams.java │ │ ├── Error.java │ │ ├── Formattable.java │ │ ├── Hash.java │ │ ├── Instance.java │ │ ├── JSONBody.java │ │ ├── Logo.java │ │ ├── OneTouch.java │ │ ├── OneTouchResponse.java │ │ ├── Params.java │ │ ├── PhoneInfo.java │ │ ├── PhoneInfoResponse.java │ │ ├── PhoneVerification.java │ │ ├── Resource.java │ │ ├── Token.java │ │ ├── Tokens.java │ │ ├── User.java │ │ ├── UserStatus.java │ │ ├── Users.java │ │ └── Verification.java └── test │ └── java │ └── com │ └── authy │ ├── TestAuthyUtil.java │ └── api │ ├── TestApiBase.java │ ├── TestOneTouch.java │ ├── TestPhoneInfo.java │ ├── TestPhoneVerification.java │ ├── TestUsers.java │ ├── TokensTest.java │ └── UserStatusTest.java └── verify-legacy-v1.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *~ 3 | target 4 | 5 | # Package Files # 6 | .settings 7 | .project 8 | .classpath 9 | bin/ 10 | build/ 11 | doc/ 12 | 13 | *.iml 14 | idea/ 15 | 16 | ### Android template 17 | # Built application files 18 | *.apk 19 | *.ap_ 20 | 21 | # Files for the ART/Dalvik VM 22 | *.dex 23 | 24 | # Java class files 25 | 26 | # Generated files 27 | gen/ 28 | out/ 29 | 30 | # Gradle files 31 | .gradle/ 32 | 33 | # Local configuration file (sdk path, etc) 34 | local.properties 35 | 36 | # Proguard folder generated by Eclipse 37 | proguard/ 38 | 39 | # Log Files 40 | *.log 41 | 42 | # Android Studio Navigation editor temp files 43 | .navigation/ 44 | 45 | # Android Studio captures folder 46 | captures/ 47 | 48 | # Intellij 49 | *.iml 50 | .idea/ 51 | # Keystore files 52 | *.jks 53 | 54 | # External native build folder generated in Android Studio 2.2 and later 55 | .externalNativeBuild 56 | ### macOS template 57 | *.DS_Store 58 | .AppleDouble 59 | .LSOverride 60 | 61 | # Icon must end with two \r 62 | Icon 63 | 64 | 65 | # Thumbnails 66 | ._* 67 | 68 | # Files that might appear in the root of a volume 69 | .DocumentRevisions-V100 70 | .fseventsd 71 | .Spotlight-V100 72 | .TemporaryItems 73 | .Trashes 74 | .VolumeIcon.icns 75 | .com.apple.timemachine.donotpresent 76 | 77 | # Directories potentially created on remote AFP share 78 | .AppleDB 79 | .AppleDesktop 80 | Network Trash Folder 81 | Temporary Items 82 | .apdisk 83 | ### JetBrains template 84 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 85 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 86 | 87 | # User-specific stuff: 88 | 89 | # Sensitive or high-churn files: 90 | .idea/dataSources/ 91 | .idea/dataSources.ids 92 | .idea/dataSources.xml 93 | .idea/dataSources.local.xml 94 | .idea/sqlDataSources.xml 95 | .idea/dynamic.xml 96 | .idea/uiDesigner.xml 97 | 98 | # Gradle: 99 | .idea/gradle.xml 100 | 101 | # Mongo Explorer plugin: 102 | .idea/mongoSettings.xml 103 | 104 | ## File-based project format: 105 | *.iws 106 | 107 | ## Plugin-specific files: 108 | 109 | # IntelliJ 110 | /out/ 111 | 112 | # mpeltonen/sbt-idea plugin 113 | .idea_modules/ 114 | 115 | # JIRA plugin 116 | atlassian-ide-plugin.xml 117 | 118 | # Crashlytics plugin (for Android Studio and IntelliJ) 119 | com_crashlytics_export_strings.xml 120 | crashlytics.properties 121 | crashlytics-build.properties 122 | fabric.properties 123 | ### Maven template 124 | target/ 125 | pom.xml.tag 126 | pom.xml.releaseBackup 127 | pom.xml.versionsBackup 128 | pom.xml.next 129 | release.properties 130 | dependency-reduced-pom.xml 131 | buildNumber.properties 132 | .mvn/timing.properties 133 | 134 | # Exclude maven wrapper 135 | !/.mvn/wrapper/maven-wrapper.jar 136 | ### JetBrains template 137 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 138 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 139 | 140 | # User-specific stuff: 141 | .idea/workspace.xml 142 | .idea/tasks.xml 143 | 144 | # Sensitive or high-churn files: 145 | 146 | # Gradle: 147 | .idea/libraries 148 | 149 | # Mongo Explorer plugin: 150 | 151 | ## File-based project format: 152 | 153 | ## Plugin-specific files: 154 | 155 | # IntelliJ 156 | 157 | # mpeltonen/sbt-idea plugin 158 | 159 | # JIRA plugin 160 | 161 | # Crashlytics plugin (for Android Studio and IntelliJ) 162 | ### macOS template 163 | 164 | # Icon must end with two \r 165 | 166 | 167 | # Thumbnails 168 | 169 | # Files that might appear in the root of a volume 170 | 171 | # Directories potentially created on remote AFP share 172 | ### Windows template 173 | # Windows image file caches 174 | Thumbs.db 175 | ehthumbs.db 176 | 177 | # Folder config file 178 | Desktop.ini 179 | 180 | # Recycle Bin used on file shares 181 | $RECYCLE.BIN/ 182 | 183 | # Windows Installer files 184 | *.cab 185 | *.msi 186 | *.msm 187 | *.msp 188 | 189 | # Windows shortcuts 190 | *.lnk 191 | ### Java template 192 | 193 | # BlueJ files 194 | *.ctxt 195 | 196 | # Mobile Tools for Java (J2ME) 197 | .mtj.tmp/ 198 | 199 | # Package Files # 200 | *.jar 201 | *.war 202 | *.ear 203 | 204 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 205 | hs_err_pid* 206 | ### Eclipse template 207 | 208 | .metadata 209 | tmp/ 210 | *.tmp 211 | *.bak 212 | *.swp 213 | *~.nib 214 | .settings/ 215 | .loadpath 216 | .recommenders 217 | 218 | # Eclipse Core 219 | 220 | # External tool builders 221 | .externalToolBuilders/ 222 | 223 | # Locally stored "Eclipse launch configurations" 224 | *.launch 225 | 226 | # PyDev specific (Python IDE for Eclipse) 227 | *.pydevproject 228 | 229 | # CDT-specific (C/C++ Development Tooling) 230 | .cproject 231 | 232 | # JDT-specific (Eclipse Java Development Tools) 233 | 234 | # Java annotation processor (APT) 235 | .factorypath 236 | 237 | # PDT-specific (PHP Development Tools) 238 | .buildpath 239 | 240 | # sbteclipse plugin 241 | .target 242 | 243 | # Tern plugin 244 | .tern-project 245 | 246 | # TeXlipse plugin 247 | .texlipse 248 | 249 | # STS (Spring Tool Suite) 250 | .springBeans 251 | 252 | # Code Recommenders 253 | .recommenders/ 254 | ### Maven template 255 | 256 | # Exclude maven wrapper 257 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - openjdk8 4 | 5 | script: "mvn cobertura:cobertura" 6 | 7 | after_success: 8 | - bash <(curl -s https://codecov.io/bash) 9 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-2020 Authy Inc 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/twilio/authy-java.svg?branch=master)](https://travis-ci.org/twilio/authy-java) 2 | [![codecov.io](http://codecov.io/github/twilio/authy-java/coverage.svg?branch=master)](https://codecov.io/gh/twilio/authy-java) 3 | 4 | 🚨🚨🚨 5 | 6 | **This library is no longer actively maintained.** The Authy API has been replaced with the [Twilio Verify API](https://www.twilio.com/docs/verify). Twilio will support the Authy API through November 1, 2022 for SMS/Voice. After this date, we’ll start to deprecate the service for SMS/Voice. Any requests sent to the API after May 1, 2023, will automatically receive an error. Push and TOTP will continue to be supported through July 2023. 7 | 8 | [Learn more about migrating from Authy to Verify.](https://www.twilio.com/blog/migrate-authy-to-verify) 9 | 10 | Please visit the Twilio Docs for: 11 | * [Verify + Java (Servlets) quickstart](https://www.twilio.com/docs/verify/quickstarts/java-servlets) 12 | * [Twilio Java helper library](https://www.twilio.com/docs/libraries/java) 13 | * [Verify API reference](https://www.twilio.com/docs/verify/api) 14 | 15 | Please direct any questions to [Twilio Support](https://support.twilio.com/hc/en-us). Thank you! 16 | 17 | 🚨🚨🚨 18 | 19 | 20 | ## Java Client for Twilio Authy Two-Factor Authentication (2FA) API 21 | 22 | Documentation for Java usage of the Authy API lives in the [official Twilio documentation](https://www.twilio.com/docs/authy/api/). 23 | 24 | The Authy API supports multiple channels of 2FA: 25 | * One-time passwords via SMS and voice. 26 | * Soft token ([TOTP](https://www.twilio.com/docs/glossary/totp) via the Authy App) 27 | * Push authentication via the Authy App 28 | 29 | If you only need SMS and Voice support for one-time passwords, we recommend using the [Twilio Verify API](https://www.twilio.com/docs/verify/api) instead. 30 | 31 | [More on how to choose between Authy and Verify here.](https://www.twilio.com/docs/verify/authy-vs-verify) 32 | 33 | ### Authy Quickstart 34 | 35 | For a full tutorial, check out either of the Java Authy Quickstarts in our docs: 36 | * [Java/Spring Authy Quickstart](https://www.twilio.com/docs/authy/quickstart/two-factor-authentication-java-spring) 37 | * [Java/Servlets Authy Quickstart](https://www.twilio.com/docs/authy/quickstart/two-factor-authentication-java-servlets) 38 | 39 | ## Authy Java Installation 40 | 41 | ### Dependencies 42 | This project uses [json.org](https://github.com/douglascrockford/JSON-java), you can find 43 | the [json.org jar versions here](https://search.maven.org/#search|gav|1|g%3A%22org.json%22%20AND%20a%3A%22json%22) 44 | 45 | * **Ant:** no need to include json.org since ant already includes it in the final jar. 46 | * **Maven:** need to include the [json.org jar](https://search.maven.org/#search|gav|1|g%3A%22org.json%22%20AND%20a%3A%22json%22) in your jar external libraries. 47 | 48 | ## Usage 49 | 50 | Add the library to the project by putting it in the dependencies section of your `pom.xml`: 51 | ``` 52 | 53 | 54 | com.authy 55 | authy-java 56 | 1.5.0 57 | 58 | ``` 59 | 60 | To use the Authy client, import the API and initialize it with your production API Key found in the [Twilio Console](https://www.twilio.com/console/authy/applications/): 61 | 62 | ```java 63 | import com.authy.*; 64 | import com.authy.api.*; 65 | 66 | AuthyApiClient client = new AuthyApiClient("your-api-key") 67 | ``` 68 | 69 | ![authy api key in console](https://s3.amazonaws.com/com.twilio.prod.twilio-docs/images/account-security-api-key.width-800.png) 70 | 71 | ## 2FA Workflow 72 | 73 | 1. [Create a user](https://www.twilio.com/docs/authy/api/users#enabling-new-user) 74 | 2. [Send a one-time password](https://www.twilio.com/docs/authy/api/one-time-passwords) 75 | 3. [Verify a one-time password](https://www.twilio.com/docs/authy/api/one-time-passwords#verify-a-one-time-password) 76 | 77 | **OR** 78 | 79 | 1. [Create a user](https://www.twilio.com/docs/authy/api/users#enabling-new-user) 80 | 2. [Send a push authentication](https://www.twilio.com/docs/authy/api/push-authentications) 81 | 3. [Check a push authentication status](https://www.twilio.com/docs/authy/api/push-authentications#check-approval-request-status) 82 | 83 | 84 | ## Phone Verification 85 | 86 | [Phone verification now lives in the Twilio API](https://www.twilio.com/docs/verify/api) and has [Java support through the official Twilio helper libraries](https://www.twilio.com/docs/libraries/java). 87 | 88 | [Legacy (V1) documentation here.](verify-legacy-v1.md) **Verify V1 is not recommended for new development. Please consider using [Verify V2](https://www.twilio.com/docs/verify/api)**. 89 | 90 | ## Copyright 91 | 92 | Copyright (c) 2011-2020 Authy Inc. See [LICENSE](https://github.com/twilio/authy-java/blob/master/LICENSE.txt) for further details. 93 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: 2 | layout: "reach, diff, flags, files" 3 | behavior: default 4 | require_changes: false # if true: only post the comment if coverage changes 5 | require_base: no # [yes :: must have a base report to post] 6 | require_head: yes # [yes :: must have a head report to post] 7 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | function install(){ 2 | mvn install:install-file -Dfile=$1 -DgroupId=$2 -DartifactId=$3 -Dversion=$4 -Dpackaging=jar 3 | } 4 | 5 | install dist/authy-java.jar com.authy authy-java 1.0 6 | 7 | # 8 | # com.authy 9 | # authy-java 10 | # 1.0 11 | # -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | com.authy 6 | authy-java 7 | 1.5.1 8 | jar 9 | Authy Java 10 | Java library to access the Authy API. 11 | https://github.com/authy/authy-java 12 | 13 | 14 | 15 | The MIT License (MIT) 16 | https://github.com/authy/authy-java/blob/master/LICENSE.txt 17 | 18 | 19 | 20 | 21 | 22 | Sergio Aristizabal 23 | saristizabal@twilio.com 24 | Twilio 25 | http://www.twilio.com 26 | 27 | 28 | 29 | 30 | UTF-8 31 | 32 | 33 | 34 | scm:git:git@github.com:authy/authy-java.git 35 | scm:git:git@github.com:authy/authy-java.git 36 | git@github.com:authy/authy-java.git 37 | 38 | 39 | 40 | 41 | org.json 42 | json 43 | 20150729 44 | 45 | 46 | junit 47 | junit 48 | 4.11 49 | test 50 | 51 | 52 | com.github.tomakehurst 53 | wiremock 54 | 2.11.0 55 | test 56 | 57 | 58 | 59 | 60 | 61 | ossrh 62 | Authy Maven Snapshot Repository 63 | 64 | https://oss.sonatype.org/content/repositories/snapshots/ 65 | 66 | 67 | 68 | 69 | ossrh 70 | Authy Maven Repository 71 | 72 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | release 81 | 82 | 83 | performRelease 84 | true 85 | 86 | 87 | 88 | 89 | 90 | org.apache.maven.plugins 91 | maven-gpg-plugin 92 | 1.5 93 | 94 | 95 | sign-artifacts 96 | verify 97 | 98 | sign 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | org.apache.maven.plugins 111 | maven-compiler-plugin 112 | 3.1 113 | 114 | 1.8 115 | 1.8 116 | 117 | 118 | 119 | 120 | org.apache.maven.plugins 121 | maven-release-plugin 122 | 123 | true 124 | false 125 | 126 | release 127 | deploy 128 | 129 | 130 | 131 | 132 | org.apache.maven.plugins 133 | maven-source-plugin 134 | 2.2.1 135 | 136 | 137 | attach-sources 138 | 139 | jar 140 | 141 | 142 | 143 | 144 | 145 | org.apache.maven.plugins 146 | maven-javadoc-plugin 147 | 2.9.1 148 | 149 | 150 | attach-javadocs 151 | 152 | jar 153 | 154 | 155 | 156 | 157 | 158 | 159 | org.sonatype.plugins 160 | nexus-staging-maven-plugin 161 | 1.6.3 162 | true 163 | 164 | ossrh 165 | https://oss.sonatype.org/ 166 | true 167 | 168 | 169 | 170 | org.codehaus.mojo 171 | cobertura-maven-plugin 172 | 2.7 173 | 174 | 175 | html 176 | xml 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | -------------------------------------------------------------------------------- /src/main/java/com/authy/AuthyApiClient.java: -------------------------------------------------------------------------------- 1 | package com.authy; 2 | 3 | import com.authy.api.*; 4 | 5 | /** 6 | * @author Julian Camargo 7 | *

8 | * Copyright © 2017 Twilio, Inc. All Rights Reserved. 9 | */ 10 | public class AuthyApiClient { 11 | public static final String CLIENT_NAME = "AuthyJava"; 12 | public static final String DEFAULT_API_URI = "https://api.authy.com"; 13 | public static final String VERSION = "1.5.0"; 14 | private Users users; 15 | private Tokens tokens; 16 | private String apiUri, apiKey; 17 | private PhoneVerification phoneVerification; 18 | private PhoneInfo phoneInfo; 19 | private OneTouch oneTouch; 20 | 21 | 22 | public AuthyApiClient(String apiKey, String apiUri) { 23 | init(apiKey, apiUri, false); 24 | } 25 | 26 | public AuthyApiClient(String apiKey) { 27 | init(apiKey, DEFAULT_API_URI, false); 28 | } 29 | 30 | public AuthyApiClient(String apiKey, String apiUri, boolean testFlag) { 31 | init(apiKey, apiUri, testFlag); 32 | } 33 | 34 | private void init(String apiKey, String apiUrl, boolean testFlag) { 35 | this.apiUri = apiUrl; 36 | this.apiKey = apiKey; 37 | 38 | this.phoneInfo = new PhoneInfo(this.apiUri, this.apiKey, testFlag); 39 | this.phoneVerification = new PhoneVerification(this.apiUri, this.apiKey, testFlag); 40 | this.users = new Users(this.apiUri, this.apiKey, testFlag); 41 | this.tokens = new Tokens(this.apiUri, this.apiKey, testFlag); 42 | this.oneTouch = new OneTouch(this.apiUri, this.apiKey, testFlag); 43 | } 44 | 45 | public Users getUsers() { 46 | return this.users; 47 | } 48 | 49 | public Tokens getTokens() { 50 | return this.tokens; 51 | } 52 | 53 | public PhoneVerification getPhoneVerification() { 54 | return this.phoneVerification; 55 | } 56 | 57 | public PhoneInfo getPhoneInfo() { 58 | return this.phoneInfo; 59 | } 60 | 61 | public OneTouch getOneTouch() { 62 | return oneTouch; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/com/authy/AuthyException.java: -------------------------------------------------------------------------------- 1 | package com.authy; 2 | 3 | import com.authy.api.Error; 4 | 5 | /** 6 | * @author Julian Camargo 7 | *

8 | * Copyright © 2017 Twilio, Inc. All Rights Reserved. 9 | */ 10 | public class AuthyException extends Exception { 11 | 12 | private final Integer status; 13 | private final Error.Code errorCode; 14 | 15 | public AuthyException(String message, Throwable throwable, Integer status) { 16 | super(message, throwable); 17 | this.status = status; 18 | this.errorCode = null; 19 | } 20 | 21 | public AuthyException(String message, Throwable throwable, Integer status, Error.Code errorCode) { 22 | super(message, throwable); 23 | this.status = status; 24 | this.errorCode = errorCode; 25 | } 26 | 27 | public AuthyException(String message, Integer status, Error.Code errorCode) { 28 | super(message); 29 | this.status = status; 30 | this.errorCode = errorCode; 31 | } 32 | 33 | public AuthyException(String message) { 34 | super(message); 35 | this.status = null; 36 | this.errorCode = null; 37 | } 38 | 39 | public AuthyException(String message, Throwable throwable) { 40 | this(message, throwable, null, null); 41 | } 42 | 43 | public int getStatus() { 44 | return status; 45 | } 46 | 47 | public Error.Code getErrorCode() { 48 | return errorCode; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/authy/AuthyUtil.java: -------------------------------------------------------------------------------- 1 | package com.authy; 2 | 3 | import org.json.JSONArray; 4 | import org.json.JSONObject; 5 | 6 | import javax.crypto.Mac; 7 | import javax.crypto.spec.SecretKeySpec; 8 | import javax.xml.bind.DatatypeConverter; 9 | import java.io.IOException; 10 | import java.io.InputStream; 11 | import java.io.UnsupportedEncodingException; 12 | import java.net.URLEncoder; 13 | import java.util.*; 14 | import java.util.logging.Level; 15 | import java.util.logging.Logger; 16 | 17 | import static java.nio.charset.StandardCharsets.UTF_8; 18 | 19 | /** 20 | * @author hansospina 21 | *

22 | * Copyright © 2017 Twilio, Inc. All Rights Reserved. 23 | */ 24 | public class AuthyUtil { 25 | public static final String HEADER_AUTHY_SIGNATURE_NONCE = "X-Authy-Signature-Nonce"; 26 | public static final String HEADER_AUTHY_SIGNATURE = "X-Authy-Signature"; 27 | 28 | private static final Logger LOGGER = Logger.getLogger(AuthyUtil.class.getName()); 29 | 30 | private static String hmacSha(String KEY, String VALUE) throws OneTouchException { 31 | 32 | try { 33 | SecretKeySpec signingKey = new SecretKeySpec(KEY.getBytes(UTF_8), "HmacSHA256"); 34 | Mac mac = Mac.getInstance("HmacSHA256"); 35 | mac.init(signingKey); 36 | byte[] rawHmac = mac.doFinal(VALUE.getBytes(UTF_8)); 37 | return DatatypeConverter.printBase64Binary(rawHmac); 38 | } catch (Exception ex) { 39 | // capture the exceptions and wrap them using authy. 40 | throw new OneTouchException("There was an exception checking the Authy OneTouch signature.", ex); 41 | } 42 | } 43 | 44 | 45 | /** 46 | * Validates the request information to 47 | * 48 | * @param parameters The request parameters(all of them) 49 | * @param headers The headers of the request 50 | * @param url The url of the request. 51 | * @param apiKey the security token from the authy library 52 | * @return true if the signature ios valid, false otherwise 53 | * @throws UnsupportedEncodingException if the string parameters have problems with UTF-8 encoding. 54 | */ 55 | private static boolean validateSignature(Map parameters, Map headers, String method, String url, String apiKey) throws OneTouchException, UnsupportedEncodingException { 56 | 57 | if (headers == null) 58 | throw new OneTouchException("No headers sent"); 59 | 60 | if (!headers.containsKey(HEADER_AUTHY_SIGNATURE)) 61 | throw new OneTouchException("'SIGNATURE' is missing."); 62 | 63 | if (!headers.containsKey(HEADER_AUTHY_SIGNATURE_NONCE)) 64 | throw new OneTouchException("'NONCE' is missing."); 65 | 66 | if (parameters == null || parameters.isEmpty()) 67 | throw new OneTouchException("'PARAMS' are missing."); 68 | 69 | StringBuilder sb = new StringBuilder(headers.get(HEADER_AUTHY_SIGNATURE_NONCE)) 70 | .append("|") 71 | .append(method) 72 | .append("|") 73 | .append(url) 74 | .append("|") 75 | .append(mapToQuery(parameters)); 76 | 77 | String signature = hmacSha(apiKey, sb.toString()); 78 | 79 | // let's check that the Authy signature is valid 80 | return signature.equals(headers.get(HEADER_AUTHY_SIGNATURE)); 81 | } 82 | 83 | 84 | public static void extract(String pre, JSONObject obj, HashMap map) { 85 | 86 | for (String k : obj.keySet()) { 87 | 88 | String key = pre.length() == 0 ? k : pre + "[" + k + "]"; 89 | if (obj.optJSONObject(k) != null) { 90 | extract(key, obj.getJSONObject(k), map); 91 | 92 | } else if (obj.optJSONArray(k) != null) { 93 | 94 | 95 | JSONArray arr = obj.getJSONArray(k); 96 | 97 | int i = 0; 98 | 99 | for (Object tmp : arr) { 100 | String tmpKey = key + "[" + i + "]"; 101 | 102 | if (tmp instanceof JSONObject) { 103 | extract(tmpKey, (JSONObject) tmp, map); 104 | } else { 105 | map.put(tmpKey, getValue(tmp)); 106 | } 107 | i++; 108 | } 109 | 110 | } else { 111 | map.put(key, getValue(obj.get(k))); 112 | } 113 | 114 | } 115 | 116 | } 117 | 118 | 119 | private static String getValue(Object val) { 120 | 121 | if (val instanceof Boolean) { 122 | return Boolean.toString(((Boolean) val)); 123 | } else if (val instanceof Integer) { 124 | return Long.toString(((Integer) val)); 125 | } else if (val instanceof Long) { 126 | return Long.toString(((Long) val)); 127 | } else if (val instanceof Float) { 128 | return Float.toString(((Float) val)); 129 | } else if (val instanceof Double) { 130 | return Double.toString(((Double) val)); 131 | } else if (JSONObject.NULL.equals(val)) { 132 | return ""; 133 | } else { 134 | return String.valueOf(val); 135 | } 136 | 137 | } 138 | 139 | public static String mapToQuery(Map map) throws OneTouchException, UnsupportedEncodingException { 140 | 141 | StringBuilder sb = new StringBuilder(); 142 | 143 | SortedSet keys = new TreeSet<>(map.keySet()); 144 | 145 | boolean first = true; 146 | 147 | for (String key : keys) { 148 | if (key.length() > 200) 149 | throw new OneTouchException("max number of characters of key exceeded."); 150 | 151 | if (first) { 152 | first = false; 153 | } else { 154 | sb.append("&"); 155 | } 156 | 157 | String value = map.get(key); 158 | 159 | // don't encode null values 160 | if (value == null) { 161 | continue; 162 | } 163 | 164 | sb.append(URLEncoder.encode(key.replaceAll("\\[([0-9])*\\]", "[]"), UTF_8.name())).append("=").append(URLEncoder.encode(value, UTF_8.name())); 165 | } 166 | 167 | return sb.toString(); 168 | 169 | } 170 | 171 | 172 | /** 173 | * Validates the request information to 174 | * 175 | * @param body The body of the request in case of a POST method 176 | * @param headers The headers of the request 177 | * @param url The url of the request. 178 | * @param apiKey the security token from the authy library 179 | * @return true if the signature ios valid, false otherwise 180 | * @throws com.authy.OneTouchException 181 | * @throws UnsupportedEncodingException if the string parameters have problems with UTF-8 encoding. 182 | */ 183 | public static boolean validateSignatureForPost(String body, Map headers, String url, String apiKey) throws OneTouchException, UnsupportedEncodingException { 184 | HashMap params = new HashMap<>(); 185 | if (body == null || body.isEmpty()) 186 | throw new OneTouchException("'PARAMS' are missing."); 187 | extract("", new JSONObject(body), params); 188 | return validateSignature(params, headers, "POST", url, apiKey); 189 | } 190 | 191 | /** 192 | * Validates the request information to 193 | * 194 | * @param params The query parameters in case of a GET request 195 | * @param headers The headers of the request 196 | * @param url The url of the request. 197 | * @param apiKey the security token from the authy library 198 | * @return true if the signature ios valid, false otherwise 199 | * @throws com.authy.OneTouchException 200 | * @throws UnsupportedEncodingException if the string parameters have problems with UTF-8 encoding. 201 | */ 202 | public static boolean validateSignatureForGet(Map params, Map headers, String url, String apiKey) throws OneTouchException, UnsupportedEncodingException { 203 | return validateSignature(params, headers, "GET", url, apiKey); 204 | } 205 | 206 | 207 | /** 208 | * Loads your api_key and api_url properties from the given property file 209 | *

210 | * Two important things to have in mind here: 211 | * 1) if api_key and api_url are defined as environment variables, they will be returned as the properties. 212 | * 2) If you want to load your properties file have in mind your classloader path may change. 213 | * 214 | * @return the Properties object containing the properties to setup Authy or an empty Properties object if no properties were found 215 | */ 216 | public static Properties loadProperties(String path, Class cls) { 217 | 218 | Properties properties = new Properties(); 219 | 220 | // environment variables will always override properties file 221 | 222 | try { 223 | 224 | InputStream in = cls.getResourceAsStream(path); 225 | 226 | // if we cant find the properties file 227 | if (in != null) { 228 | properties.load(in); 229 | } 230 | 231 | 232 | // Env variables will always override properties 233 | if (System.getenv("api_key") != null && System.getenv("api_url") != null) { 234 | properties.put("api_key", System.getenv("api_key")); 235 | properties.put("api_url", System.getenv("api_url")); 236 | } 237 | 238 | } catch (IOException e) { 239 | LOGGER.log(Level.SEVERE, "Problems loading properties", e); 240 | } 241 | 242 | 243 | return properties; 244 | } 245 | 246 | } 247 | -------------------------------------------------------------------------------- /src/main/java/com/authy/OneTouchException.java: -------------------------------------------------------------------------------- 1 | package com.authy; 2 | 3 | /** 4 | * @author hansospina 5 | *

6 | * Copyright © 2017 Twilio, Inc. All Rights Reserved. 7 | */ 8 | public class OneTouchException extends AuthyException { 9 | 10 | public OneTouchException(String message, Throwable throwable) { 11 | super(message, throwable); 12 | } 13 | 14 | public OneTouchException(String message) { 15 | super(message); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/authy/api/ApprovalRequestParams.java: -------------------------------------------------------------------------------- 1 | /* 2 | * To change this license header, choose License Headers in Project Properties. 3 | * To change this template file, choose Tools | Templates 4 | * and open the template in the editor. 5 | */ 6 | package com.authy.api; 7 | 8 | import com.authy.OneTouchException; 9 | 10 | import java.util.ArrayList; 11 | import java.util.HashMap; 12 | import java.util.List; 13 | 14 | /** 15 | * @author hansospina 16 | *

17 | * Copyright © 2017 Twilio, Inc. All Rights Reserved. 18 | */ 19 | public class ApprovalRequestParams { 20 | 21 | private HashMap details = new HashMap(); 22 | private HashMap hidden = new HashMap(); 23 | private List logos = new ArrayList<>(); 24 | private Long secondsToExpire; 25 | private Integer authyId; 26 | private String message; 27 | 28 | // lock the main constructor 29 | private ApprovalRequestParams() { 30 | } 31 | 32 | public HashMap getDetails() { 33 | return details; 34 | } 35 | 36 | public HashMap getHidden() { 37 | return hidden; 38 | } 39 | 40 | public List getLogos() { 41 | return logos; 42 | } 43 | 44 | public Long getSecondsToExpire() { 45 | return secondsToExpire; 46 | } 47 | 48 | public Integer getAuthyId() { 49 | return authyId; 50 | } 51 | 52 | public String getMessage() { 53 | return message; 54 | } 55 | 56 | public enum Resolution { 57 | Default("default"), 58 | Low("low"), 59 | Medium("med"), 60 | High("high"); 61 | 62 | private String res; 63 | 64 | Resolution(String res) { 65 | this.res = res; 66 | } 67 | 68 | public String getRes() { 69 | return res; 70 | } 71 | 72 | 73 | } 74 | 75 | public static class Builder { 76 | 77 | 78 | public static final String MESSAGE_ERROR = "Param message cannot be null or empty and it's length needs to be less than 200 max characters."; 79 | public static final String AUTHYID_ERROR = "Param authyId cannot be null and should be the id of the user that will be authorized using OneTouch."; 80 | public static final String DETAIL_ERROR = "Each entry(key,value) for a detail needs to have not null or empty values and keys and it's lengths cannot exceed 200 max characters."; 81 | public static final String HIDDEN_DETAIL_ERROR = "Each entry(key,value) for a hidden detail needs to have not null or empty values and keys and it's lengths cannot exceed 200 max characters."; 82 | public static final String LOGO_ERROR_RES = "The 'Resolution' for a logo cannot be null."; 83 | public static final String LOGO_ERROR_URL = "The 'url' for a logo cannot be null or empty and it's length needs to be less than 500 max characters."; 84 | public static final String LOGO_ERROR_DEFAULT = "If you provide logos you should always provide the default Resolution."; 85 | private static final int MAXSIZE = 200; 86 | private static final int MAXSIZEURL = 500; 87 | ApprovalRequestParams params = new ApprovalRequestParams(); 88 | private HashMap currentLogos = new HashMap<>(); 89 | 90 | /** 91 | * This will create a valid builder with the initial required params. 92 | * See: https://www.twilio.com/docs/api/authy/authy-onetouch-api#parameters 93 | * 94 | * @param authyId The id of the user that will be authorized using OneTouch 95 | * @param message The message to show to the user, cannot be null or empty and it's length needs to be less than 200 max characters. 96 | * @throws OneTouchException If any of the params doesn't match the required/length rules. 97 | */ 98 | public Builder(Integer authyId, String message) throws OneTouchException { 99 | 100 | if (authyId == null) { 101 | throw new OneTouchException(AUTHYID_ERROR); 102 | } 103 | 104 | if (message == null || message.isEmpty() || message.length() > MAXSIZE) { 105 | throw new OneTouchException(MESSAGE_ERROR); 106 | } 107 | 108 | this.params.authyId = authyId; 109 | this.params.message = message; 110 | } 111 | 112 | /** 113 | * Defines the second to expire parameter 114 | *

115 | * See: https://www.twilio.com/docs/api/authy/authy-onetouch-api#parameters 116 | * 117 | * @param secondsToExpire Number of seconds that the approval request will be available for being responded. 118 | */ 119 | public Builder setSecondsToExpire(Long secondsToExpire) { 120 | // this parameter can be null or any long value so we don't need to validate anything else. 121 | this.params.secondsToExpire = secondsToExpire; 122 | return this; 123 | } 124 | 125 | /** 126 | * Adds a key,value pair into the details which is a Dictionary containing the [ApprovalRequest] details that will be shown to user. 127 | *

128 | * See: https://www.twilio.com/docs/api/authy/authy-onetouch-api#parameters 129 | * 130 | * @param key The label of the detail that will be shown to the user, cannot be null or empty and it's length needs to be less than 200 max characters. 131 | * @param value The value of the detail that will be shown to the user, cannot be null or empty and it's length needs to be less than 200 max characters. 132 | * @throws OneTouchException If any of the params doesn't match the required/length rules. 133 | */ 134 | public Builder addDetail(String key, String value) throws OneTouchException { 135 | 136 | if (key == null || key.isEmpty() || key.length() > MAXSIZE) { 137 | throw new OneTouchException(DETAIL_ERROR); 138 | } 139 | 140 | if (value == null || value.isEmpty() || value.length() > MAXSIZE) { 141 | throw new OneTouchException(DETAIL_ERROR); 142 | } 143 | 144 | this.params.details.put(key, value); 145 | 146 | return this; 147 | } 148 | 149 | /** 150 | * Adds a key,value pair into the hidden_details which is a Dictionary containing the approval request details hidden to user. 151 | *

152 | * See: https://www.twilio.com/docs/api/authy/authy-onetouch-api#parameters 153 | * 154 | * @param key The label of the hidden detail that will be not shown to the user, cannot be null or empty and it's length needs to be less than 200 max characters. 155 | * @param value The value of the hidden detail that will be not shown to the user, cannot be null or empty and it's length needs to be less than 200 max characters. 156 | * @throws OneTouchException If any of the params doesn't match the required/length rules. 157 | */ 158 | public Builder addHiddenDetail(String key, String value) throws OneTouchException { 159 | 160 | if (key == null || key.isEmpty() || key.length() > MAXSIZE) { 161 | throw new OneTouchException(HIDDEN_DETAIL_ERROR); 162 | } 163 | 164 | if (value == null || value.isEmpty() || value.length() > MAXSIZE) { 165 | throw new OneTouchException(HIDDEN_DETAIL_ERROR); 166 | } 167 | 168 | this.params.hidden.put(key, value); 169 | 170 | return this; 171 | } 172 | 173 | 174 | /** 175 | * Adds a new logo item to the Logos array. 176 | * 177 | * @param resolution The Resolution wanted for the given logo(Default, log,med,high) 178 | * @param url The url to be used to load the logo. 179 | * @throws OneTouchException If any of the params doesn't match the required/length rules. 180 | */ 181 | public Builder addLogo(Resolution resolution, String url) throws OneTouchException { 182 | 183 | if (resolution == null) { 184 | throw new OneTouchException(LOGO_ERROR_RES); 185 | } 186 | 187 | if (url == null || url.isEmpty() || url.length() > MAXSIZEURL) { 188 | throw new OneTouchException(LOGO_ERROR_URL); 189 | } 190 | 191 | // let's use a map to prevent duplicates in the logo list, 192 | // if a new logo with an already used resolution comes we will just replace it. 193 | this.currentLogos.put(resolution, new Logo(resolution, url)); 194 | return this; 195 | } 196 | 197 | 198 | /** 199 | * Compiles and creates the provided set of parameters to have a ready to use ApprovalRequestParams object. 200 | * 201 | * @return The bean containing all the properties required to send a valid OneTouch request to Authy. 202 | * @throws OneTouchException If any of the params doesn't match the required/length rules. 203 | */ 204 | public ApprovalRequestParams build() throws OneTouchException { 205 | 206 | // if we have logo but the user didnt send a default this 207 | if (!currentLogos.isEmpty() && !currentLogos.containsKey(Resolution.Default)) { 208 | throw new OneTouchException(LOGO_ERROR_DEFAULT); 209 | } 210 | 211 | this.params.logos.addAll(currentLogos.values()); 212 | 213 | 214 | return this.params; 215 | } 216 | 217 | 218 | } 219 | 220 | 221 | } 222 | -------------------------------------------------------------------------------- /src/main/java/com/authy/api/Error.java: -------------------------------------------------------------------------------- 1 | package com.authy.api; 2 | 3 | import javax.xml.bind.JAXBContext; 4 | import javax.xml.bind.Marshaller; 5 | import javax.xml.bind.annotation.XmlElement; 6 | import javax.xml.bind.annotation.XmlRootElement; 7 | import java.io.StringWriter; 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | 11 | @XmlRootElement(name = "errors") 12 | public class Error implements Formattable { 13 | 14 | public enum Code { 15 | DEFAULT_ERROR(6000), 16 | API_KEY_INVALID(60001), 17 | INVALID_REQUEST(60002), 18 | INVALID_PARAMETER(60004), 19 | INVALID_ENCODING(60005), 20 | TOKEN_VIA_CALL_ADDON_DISABLED(60006), 21 | SMS_DISABLED(60007), 22 | ACCOUNT_SUSPENDED(60008), 23 | MONTHLY_SMS_LIMIT_REACHED(60009), 24 | DAILY_SMS_LIMIT_REACHED(60010), 25 | MONTHLY_CALLS_LIMIT_REACHED(60011), 26 | DAILY_CALLS_LIMIT_REACHED(60012), 27 | BANNED_COUNTRY(60013), 28 | CALL_NOT_STARTED(60014), 29 | SMS_NOT_SENT(60015), 30 | USER_DOES_NOT_EXIST(60016), 31 | USER_SUSPENDED(60017), 32 | USER_DISABLED(60018), 33 | REUSED_TOKEN(60019), 34 | TOKEN_INVALID(60020), 35 | CANNOT_CREATE_PHONE_VERIFICATION(60021), 36 | PHONE_VERIFICATION_INCORRECT(60022), 37 | PHONE_VERIFICATION_NOT_FOUND(60023), 38 | CANNOT_GET_PHONE_INFO(60024), 39 | PHONE_INFO_ERROR_QUERYING(60025), 40 | USER_NOT_FOUND(60026), 41 | USER_NOT_VALID(60027), 42 | COULD_NOT_DELETE_USER(60028), 43 | CANNOT_CREATE_ACTIVITY(60029), 44 | USER_INCORRECT_PARAMS(60030), 45 | ACTION_NOT_AUTHORIZED(60031), 46 | SMS_NOT_FOUND(60032), 47 | INVALID_PHONE_NUMBER(60033), 48 | REGISTRATION_REQUEST_INVALID(60034), 49 | REGISTRATION_REQUEST_NOT_FOUND(60035), 50 | REGISTRATION_INVALID_PIN(60036), 51 | REGISTRATION_EXPIRED(60037), 52 | INVALID_EMAIL(60038), 53 | PHONE_VERIFICATION_PARAMS_INVALID(60042), 54 | TWILIO_API_KEY_DETECTED(60047), 55 | ONETOUCH_APPROVAL_REQUEST_NOT_FOUND(60049), 56 | ONETOUCH_UNREGISTERED_USER(60050), 57 | ONETOUCH_DEVICE_NOT_FOUND(60051), 58 | ONETOUCH_INTERNAL_CONNECTION_ERROR(60052), 59 | ONETOUCH_SENDING_APPROVAL_REQUEST_ERROR(60053), 60 | ONETOUCH_APPROVAL_REQUEST_ERROR(60054), 61 | ONETOUCH_NOTIFYING_CUSTOMER_ERROR(60055), 62 | MUST_USE_SSL(60056), 63 | ACCOUNT_SUSPENDED_TEMPORARILY(60057), 64 | PHONE_NUMBER_NOT_FOUND(60058), 65 | PHONE_NUMBER_INVALID(60059), 66 | TWILIO_ACCOUNT_SUSPENDED(60060), 67 | APPLICATION_SUSPENDED(60061), 68 | DISALLOWED_IP(60063), 69 | CANNOT_ENABLE_ONETOUCH(60064), 70 | ONETOUCH_CANNOT_SAVE_CALLBACK(60066), 71 | CANNOT_UPDATE_ON_DEVICE_REGISTRATION(60068), 72 | ACCESS_KEY_ERROR(60069), 73 | INVALID_APPLICATION(60070), 74 | ACCESS_KEY_NOT_FOUND(60071), 75 | INVALID_ACCESS_KEY(60072), 76 | INVALID_APPLICATION_API_KEY(60073), 77 | ACCESS_KEY_PERMISSION_DENIED(60074), 78 | CANNOT_DELETE_APPLICATION(60075), 79 | COUNTRY_CODE_VALIDATION_FAIL(60078), 80 | ONETOUCH_APPROVAL_REQUEST_NOT_PENDING(60079), 81 | ONETOUCH_APPROVAL_REQUEST_INVALID(60080), 82 | CANNOT_SEND_SMS_TO_LANDLINE(60082), 83 | PHONE_NUMBER_NOT_PROVISIONED(60083), 84 | JWT_TOKEN_EXPIRED(60086), 85 | INVALID_SIGNATURE(60087), 86 | INVALID_REPORTING_QUERY(60089), 87 | REGISTRATION_REQUEST_COULD_NOT_BE_CREATED(60090), 88 | CUSTOM_MESSAGE_DISALLOWED(60091), 89 | DEVICE_NOT_FOUND(60092), 90 | SDK_DEVICE_NOT_DELETED(60093), 91 | INVALID_REPORTING_INTERVAL(60094), 92 | INVALID_REPORTING_REPORT(60095), 93 | ERROR_PROCESSING_REPORT(60096), 94 | PHONE_CHANGE_IN_PROGRESS(60097), 95 | WEBHOOK_CREATION_ERROR(60098), 96 | WEBHOOK_LIST_ERROR(60099), 97 | WEBHOOK_DELETION_ERROR(60100), 98 | INVALID_JWT_TOKEN(60101), 99 | PUSH_CERT_CREATION_ERROR(60102), 100 | NOT_RECOGNIZED_PUSH_PLATFORM(60103), 101 | INVALID_PUSH_CERTS(60104), 102 | NOTIFY_JWT_TOKEN_ERROR(60105), 103 | USER_SUSPENDED_FROM_APP(60106), 104 | USER_BLOCKED(60107), 105 | INVALID_CHANNEL_FOR_DEVICE(60108), 106 | AUTHENTICATION_METHOD_NOT_FOUND(60109), 107 | AUTHENTICATION_METHOD_CANNOT_BE_CREATED(60110), 108 | AUTHENTICATION_NOT_FOUND(60111), 109 | INVALID_AUTHENTICATION_METHOD(60112), 110 | AUTHENTICATOR_NOT_FOUND(60113), 111 | AUTHENTICATOR_CANNOT_BE_UPDATED(60114), 112 | NUMBER_OPTED_OUT(60115), 113 | BAD_PV_JWT_PARAMS(60116), 114 | APPLICATION_NOT_FOUND(60117), 115 | TOTP_CODE_INVALID(60118), 116 | USER_WITHOUT_PII_REQUIRED(60119), 117 | SMS_LIMIT_REACHED(60120), 118 | SMS_INVALID(60121), 119 | QR_CODE_GENERATION_FAILED(60122), 120 | GENERIC_TOKENS_DISABLED(60123), 121 | ACCOUNT_NOT_FOUND(60124), 122 | INVALID_SDK_APP(60125), 123 | HLR_REPORT_ERROR(60126), 124 | USER_PENDING_FOR_DELETION(60127), 125 | USER_WAS_DELETED(60128), 126 | PUBLIC_KEY_NOT_FOUND(60129), 127 | USER_DELETION_ON_GOING(60130), 128 | USER_DELETION_FAILED(60131), 129 | ACCOUNT_DELETION_INCOMPLETE(60132), 130 | CLNPC_MESSAGE(60133) ; 131 | 132 | private final int number; 133 | 134 | Code(int number) { 135 | this.number = number; 136 | } 137 | 138 | public int getNumber() { 139 | return number; 140 | } 141 | } 142 | 143 | private String message, url, countryCode; 144 | private Code code; 145 | 146 | @XmlElement(name = "country-code") 147 | public String getCountryCode() { 148 | return countryCode; 149 | } 150 | 151 | public void setCountryCode(String countryCode) { 152 | this.countryCode = countryCode; 153 | } 154 | 155 | @XmlElement(name = "message") 156 | public String getMessage() { 157 | return message; 158 | } 159 | 160 | public void setMessage(String message) { 161 | this.message = message; 162 | } 163 | 164 | @XmlElement(name = "url") 165 | public String getUrl() { 166 | return url; 167 | } 168 | 169 | public Code getCode() { 170 | return code; 171 | } 172 | 173 | public void setCode(Code code) { 174 | this.code = code; 175 | } 176 | 177 | public void setUrl(String url) { 178 | this.url = url; 179 | } 180 | 181 | /** 182 | * Map a Token instance to its XML representation. 183 | * 184 | * @return a String with the description of this object in XML. 185 | */ 186 | public String toXML() { 187 | StringWriter sw = new StringWriter(); 188 | String xml = ""; 189 | 190 | try { 191 | JAXBContext context = JAXBContext.newInstance(this.getClass()); 192 | Marshaller marshaller = context.createMarshaller(); 193 | 194 | marshaller.marshal(this, sw); 195 | xml = sw.toString(); 196 | } catch (Exception e) { 197 | e.printStackTrace(); 198 | } 199 | return xml; 200 | } 201 | 202 | /** 203 | * Map a Token instance to its Java's Map representation. 204 | * 205 | * @return a Java's Map with the description of this object. 206 | */ 207 | public Map toMap() { 208 | Map map = new HashMap<>(); 209 | 210 | map.put("message", message); 211 | map.put("country-code", countryCode); 212 | map.put("url", url); 213 | 214 | return map; 215 | } 216 | 217 | @Override 218 | public String toString() { 219 | return "Error [message=" + message + ", url=" + url + ", countryCode=" 220 | + countryCode + "]"; 221 | } 222 | 223 | } 224 | -------------------------------------------------------------------------------- /src/main/java/com/authy/api/Formattable.java: -------------------------------------------------------------------------------- 1 | package com.authy.api; 2 | 3 | import org.json.JSONObject; 4 | 5 | import java.util.Map; 6 | 7 | /** 8 | * Interface to represent objects as XML or Java's Map 9 | * 10 | * @author Authy Inc 11 | */ 12 | public interface Formattable { 13 | String toXML(); 14 | 15 | Map toMap(); 16 | 17 | default String toJSON() { 18 | JSONObject json = new JSONObject(); 19 | for (Map.Entry entry : toMap().entrySet()) { 20 | json.put(entry.getKey(), entry.getValue()); 21 | } 22 | 23 | return json.toString(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/authy/api/Hash.java: -------------------------------------------------------------------------------- 1 | package com.authy.api; 2 | 3 | import javax.xml.bind.annotation.XmlElement; 4 | import javax.xml.bind.annotation.XmlRootElement; 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | 8 | /** 9 | * @author Julian Camargo 10 | *

11 | * Copyright © 2017 Twilio, Inc. All Rights Reserved. 12 | 13 | */ 14 | @XmlRootElement(name = "hash") 15 | public class Hash extends Instance implements Formattable { 16 | 17 | private User user = null; 18 | private String token; 19 | private boolean success; 20 | 21 | public Hash() { 22 | } 23 | 24 | public Hash(int status, String content) { 25 | super(status, content); 26 | } 27 | 28 | @XmlElement(type = User.class) 29 | public User getUser() { 30 | return user; 31 | } 32 | 33 | public void setUser(User user) { 34 | this.user = user; 35 | } 36 | 37 | public String getMessage() { 38 | return message; 39 | } 40 | 41 | public void setMessage(String message) { 42 | this.message = message; 43 | } 44 | 45 | public String getToken() { 46 | return token; 47 | } 48 | 49 | public void setToken(String token) { 50 | this.token = token; 51 | } 52 | 53 | public boolean isSuccess() { 54 | return success; 55 | } 56 | 57 | public void setSuccess(boolean success) { 58 | this.success = success; 59 | } 60 | 61 | /** 62 | * Map a Token instance to its Java's Map representation. 63 | * 64 | * @return a Java's Map with the description of this object. 65 | */ 66 | public Map toMap() { 67 | 68 | HashMap map = new HashMap<>(); 69 | 70 | if( user != null ) { 71 | 72 | Map userMap = user.toMap(); 73 | 74 | for(String st : userMap.keySet() ){ 75 | map.put("user."+st,userMap.get(st)); 76 | } 77 | 78 | } 79 | 80 | map.put("message",message); 81 | map.put("token",token); 82 | map.put("success",String.valueOf(success)); 83 | return map; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/com/authy/api/Instance.java: -------------------------------------------------------------------------------- 1 | package com.authy.api; 2 | 3 | import java.io.StringWriter; 4 | 5 | import javax.xml.bind.JAXBContext; 6 | import javax.xml.bind.Marshaller; 7 | 8 | /** 9 | * Generic class to instance a response from the API 10 | * 11 | * @author Julian Camargo 12 | */ 13 | 14 | public class Instance { 15 | int status; 16 | String content; 17 | String message; 18 | Error error; 19 | 20 | public Instance() { 21 | content = ""; 22 | } 23 | 24 | public Instance(int status, String content) { 25 | this.status = status; 26 | this.content = content; 27 | } 28 | 29 | public Instance(int status, String content, String message) { 30 | this.status = status; 31 | this.content = content; 32 | this.message = message; 33 | } 34 | 35 | /** 36 | * Check if this is instance is correct. (i.e No error occurred) 37 | * 38 | * @return true if no error occurred else false. 39 | */ 40 | public boolean isOk() { 41 | return status == 200; 42 | } 43 | 44 | /** 45 | * Return an Error object with the error that have occurred or null. 46 | * 47 | * @return an Error object 48 | */ 49 | public Error getError() { 50 | return error; 51 | } 52 | 53 | /** 54 | * Set an Error object. 55 | */ 56 | public void setError(Error error) { 57 | this.error = error; 58 | } 59 | 60 | public int getStatus() { 61 | return this.status; 62 | } 63 | 64 | public void setStatus(int status) { 65 | this.status = status; 66 | } 67 | 68 | /** 69 | * Map a Token instance to its XML representation. 70 | * 71 | * @return a String with the description of this object in XML. 72 | */ 73 | public String toXML() { 74 | Error error = getError(); 75 | 76 | if (error != null) { 77 | return error.toXML(); 78 | } 79 | 80 | StringWriter sw = new StringWriter(); 81 | String xml = ""; 82 | 83 | try { 84 | JAXBContext context = JAXBContext.newInstance(this.getClass()); 85 | Marshaller marshaller = context.createMarshaller(); 86 | 87 | marshaller.marshal(this, sw); 88 | xml = sw.toString(); 89 | } catch (Exception e) { 90 | e.printStackTrace(); 91 | } 92 | return xml; 93 | } 94 | } 95 | 96 | -------------------------------------------------------------------------------- /src/main/java/com/authy/api/JSONBody.java: -------------------------------------------------------------------------------- 1 | package com.authy.api; 2 | 3 | import com.authy.AuthyUtil; 4 | import org.json.JSONObject; 5 | 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | 9 | /** 10 | * @author hansospina 11 | *

12 | * Copyright © 2017 Twilio, Inc. All Rights Reserved. 13 | */ 14 | public class JSONBody implements Formattable { 15 | 16 | private JSONObject obj; 17 | 18 | public JSONBody(JSONObject obj) { 19 | this.obj = obj != null ? obj : new JSONObject(); 20 | } 21 | 22 | public String toXML() { 23 | return null; 24 | } 25 | 26 | public Map toMap() { 27 | 28 | HashMap map = new HashMap<>(); 29 | AuthyUtil.extract("", obj, map); 30 | 31 | return map; 32 | } 33 | 34 | public String toJSON() { 35 | return obj.toString(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/authy/api/Logo.java: -------------------------------------------------------------------------------- 1 | /* 2 | * To change this license header, choose License Headers in Project Properties. 3 | * To change this template file, choose Tools | Templates 4 | * and open the template in the editor. 5 | */ 6 | package com.authy.api; 7 | 8 | import org.json.JSONArray; 9 | import org.json.JSONObject; 10 | 11 | /** 12 | * @author hansospina 13 | *

14 | * Copyright © 2017 Twilio, Inc. All Rights Reserved. 15 | */ 16 | public class Logo { 17 | 18 | 19 | private final int MAX = 201; 20 | private String res; 21 | private String url; 22 | 23 | 24 | public Logo(ApprovalRequestParams.Resolution res, String url) { 25 | this.res = res.getRes(); 26 | if (url == null) 27 | this.url = ""; 28 | else 29 | this.url = url.substring(0, Math.min(url.length(), MAX)); 30 | } 31 | 32 | public String getRes() { 33 | return res; 34 | } 35 | 36 | public void setRes(ApprovalRequestParams.Resolution res) { 37 | this.res = res.getRes(); 38 | } 39 | 40 | public String getUrl() { 41 | return url; 42 | } 43 | 44 | public void setUrl(String url) { 45 | if (url == null) 46 | this.url = ""; 47 | else 48 | this.url = url.substring(0, Math.min(url.length(), MAX)); 49 | } 50 | 51 | public void addToMap(JSONArray map) { 52 | if (!getUrl().isEmpty()) { 53 | JSONObject temp = new JSONObject(); 54 | temp.put("res", getRes()); 55 | temp.put("url", getUrl()); 56 | map.put(temp); 57 | } 58 | } 59 | 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/authy/api/OneTouch.java: -------------------------------------------------------------------------------- 1 | package com.authy.api; 2 | 3 | import com.authy.AuthyException; 4 | import com.authy.OneTouchException; 5 | 6 | import org.json.JSONArray; 7 | import org.json.JSONObject; 8 | 9 | import java.net.URLEncoder; 10 | import java.util.Map; 11 | 12 | /** 13 | * @author hansospina 14 | *

15 | * Copyright © 2017 Twilio, Inc. All Rights Reserved. 16 | */ 17 | public class OneTouch extends Resource { 18 | 19 | public static final String APPROVAL_REQUEST_PRE = "/onetouch/json/users/"; 20 | public static final String APPROVAL_REQUEST_POS = "/approval_requests"; 21 | public static final String APPROVAL_REQUEST_STATUS = "/onetouch/json/approval_requests/"; 22 | 23 | 24 | public OneTouch(String uri, String key) { 25 | super(uri, key, Resource.JSON_CONTENT_TYPE); 26 | 27 | } 28 | 29 | public OneTouch(String uri, String apiKey, boolean testFlag) { 30 | super(uri, apiKey, testFlag, Resource.JSON_CONTENT_TYPE); 31 | } 32 | 33 | 34 | /** 35 | * Sends the OneTouch's approval request to the Authy servers and returns the OneTouchResponse that comes back. 36 | * 37 | * @param approvalRequestParams The bean wrapping the user's Authy approval request built using the ApprovalRequest.Builder 38 | * @return The bean wrapping the response from Authy's service. 39 | */ 40 | public OneTouchResponse sendApprovalRequest(ApprovalRequestParams approvalRequestParams) 41 | throws AuthyException {//Integer userId, String message, HashMap options, Integer secondsToExpire) throws OneTouchException { 42 | 43 | 44 | JSONObject params = new JSONObject(); 45 | params.put("message", approvalRequestParams.getMessage()); 46 | 47 | 48 | if (approvalRequestParams.getSecondsToExpire() != null) { 49 | params.put("seconds_to_expire", approvalRequestParams.getSecondsToExpire()); 50 | } 51 | 52 | if (approvalRequestParams.getDetails().size() > 0) { 53 | params.put("details", mapToJSONObject(approvalRequestParams.getDetails())); 54 | } 55 | 56 | 57 | if (approvalRequestParams.getHidden().size() > 0) { 58 | params.put("hidden_details", mapToJSONObject(approvalRequestParams.getHidden())); 59 | } 60 | 61 | 62 | if (!approvalRequestParams.getLogos().isEmpty()) { 63 | JSONArray jSONArray = new JSONArray(); 64 | 65 | for (Logo logo : approvalRequestParams.getLogos()) { 66 | logo.addToMap(jSONArray); 67 | } 68 | 69 | params.put("logos", jSONArray); 70 | } 71 | 72 | final Response response = this.post(APPROVAL_REQUEST_PRE + approvalRequestParams.getAuthyId() + APPROVAL_REQUEST_POS, new JSONBody(params)); 73 | OneTouchResponse oneTouchResponse = new OneTouchResponse(response.getStatus(), response.getBody()); 74 | 75 | if (!oneTouchResponse.isOk()) { 76 | oneTouchResponse.setError(errorFromJson(response.getBody())); 77 | } 78 | return oneTouchResponse; 79 | } 80 | 81 | public OneTouchResponse getApprovalRequestStatus(String uuid) throws OneTouchException { 82 | 83 | try { 84 | final Response response = this.get(APPROVAL_REQUEST_STATUS + URLEncoder.encode(uuid, ENCODE), new Params()); 85 | OneTouchResponse oneTouchResponse = new OneTouchResponse(response.getStatus(), response.getBody()); 86 | if (!oneTouchResponse.isOk()) { 87 | oneTouchResponse.setError(errorFromJson(response.getBody())); 88 | } 89 | return oneTouchResponse; 90 | 91 | } catch (Exception e) { 92 | throw new OneTouchException("There was an error trying to process this request.", e); 93 | } 94 | 95 | } 96 | 97 | 98 | private JSONObject mapToJSONObject(Map map) { 99 | 100 | JSONObject obj = new JSONObject(); 101 | 102 | for (String key : map.keySet()) { 103 | obj.put(key, map.get(key)); 104 | } 105 | 106 | return obj; 107 | } 108 | 109 | 110 | } 111 | -------------------------------------------------------------------------------- /src/main/java/com/authy/api/OneTouchResponse.java: -------------------------------------------------------------------------------- 1 | package com.authy.api; 2 | 3 | import com.authy.AuthyException; 4 | 5 | import org.json.JSONException; 6 | import org.json.JSONObject; 7 | 8 | /** 9 | * @author hansospina 10 | *

11 | * Copyright © 2017 Twilio, Inc. All Rights Reserved. 12 | */ 13 | public class OneTouchResponse extends Instance { 14 | 15 | private JSONObject obj; 16 | 17 | 18 | public OneTouchResponse(int status, String content) throws AuthyException { 19 | super(status, content); 20 | try { 21 | obj = new JSONObject(content); 22 | } catch (JSONException ex) { 23 | throw new AuthyException("Invalid JSON format, the given string is not a valid json object.", ex); 24 | } 25 | } 26 | 27 | public OneTouchResponse(String json) throws AuthyException { 28 | this(200, json); 29 | } 30 | 31 | public boolean isSuccess() { 32 | return obj.has("success") && obj.getBoolean("success"); 33 | } 34 | 35 | public String getMessage() { 36 | return obj.has("message") ? obj.getString("message") : ""; 37 | } 38 | 39 | public String getErrorCode() { 40 | return obj.has("error_code") ? obj.getString("error_code") : ""; 41 | } 42 | 43 | 44 | public ApprovalRequest getApprovalRequest() { 45 | 46 | if (obj.has("approval_request")) { 47 | return new ApprovalRequest(); 48 | } 49 | 50 | return null; 51 | } 52 | 53 | public class ApprovalRequest { 54 | 55 | private ApprovalRequest() { 56 | } 57 | 58 | public boolean isNotified() { 59 | return obj.getJSONObject("approval_request").has("notified") && obj.getJSONObject("approval_request").getBoolean("notified"); 60 | } 61 | 62 | public String createdAt() { 63 | return obj.getJSONObject("approval_request").has("created_at") ? obj.getJSONObject("approval_request").getString("created_at") : null; 64 | } 65 | 66 | public String getUUID() { 67 | return obj.getJSONObject("approval_request").has("uuid") ? obj.getJSONObject("approval_request").getString("uuid") : null; 68 | } 69 | 70 | public String getStatus() { 71 | return obj.getJSONObject("approval_request").has("status") ? obj.getJSONObject("approval_request").getString("status") : null; 72 | } 73 | 74 | // if the user was a value that is not mapped previously 75 | public String getValue(String key) { 76 | return obj.getJSONObject("approval_request").has(key) ? obj.getJSONObject("approval_request").getString(key) : null; 77 | } 78 | } 79 | 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/com/authy/api/Params.java: -------------------------------------------------------------------------------- 1 | package com.authy.api; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | /** 7 | * @author Authy Inc 8 | */ 9 | 10 | public class Params implements Formattable { 11 | private Map data; 12 | 13 | public Params() { 14 | data = new HashMap<>(); 15 | } 16 | 17 | public void setAttribute(String key, String value) { 18 | this.data.put(key, value); 19 | } 20 | 21 | // required to satisfy Formattable interface 22 | public String toXML() { 23 | return ""; 24 | } 25 | 26 | public Map toMap() { 27 | return this.data; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/authy/api/PhoneInfo.java: -------------------------------------------------------------------------------- 1 | package com.authy.api; 2 | 3 | import com.authy.AuthyException; 4 | 5 | /** 6 | * @author Moisés Vargas 7 | */ 8 | public class PhoneInfo extends Resource { 9 | public static final String PHONE_INFO_API_PATH = "/protected/json/phones/"; 10 | 11 | public PhoneInfo(String uri, String key) { 12 | super(uri, key, Resource.JSON_CONTENT_TYPE); 13 | } 14 | 15 | public PhoneInfo(String uri, String key, boolean testFlag) { 16 | super(uri, key, testFlag, Resource.JSON_CONTENT_TYPE); 17 | } 18 | 19 | public PhoneInfoResponse info(String phoneNumber, String countryCode) throws AuthyException { 20 | return info(phoneNumber, countryCode, new Params()); 21 | } 22 | 23 | public PhoneInfoResponse info(String phoneNumber, String countryCode, Params params) throws AuthyException { 24 | params.setAttribute("phone_number", phoneNumber); 25 | params.setAttribute("country_code", countryCode); 26 | final Response response = this.get(PHONE_INFO_API_PATH + "info", params); 27 | PhoneInfoResponse info = new PhoneInfoResponse(response.getStatus(), response.getBody()); 28 | if (!info.isOk()) { 29 | info.setError(errorFromJson(response.getBody())); 30 | } 31 | return info; 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/authy/api/PhoneInfoResponse.java: -------------------------------------------------------------------------------- 1 | package com.authy.api; 2 | 3 | import org.json.JSONObject; 4 | 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | 8 | /** 9 | * @author Moisés Vargas 10 | */ 11 | 12 | public class PhoneInfoResponse extends Instance implements Formattable { 13 | private String message = "Something went wrong!"; 14 | private String provider = ""; 15 | private String type = ""; 16 | private boolean isPorted = false; 17 | 18 | public PhoneInfoResponse() { 19 | } 20 | 21 | public PhoneInfoResponse(int status, String response) { 22 | this(status, response, null); 23 | } 24 | 25 | public PhoneInfoResponse(int status, String response, String message) { 26 | this.status = status; 27 | this.content = response; 28 | this.message = message; 29 | this.setResponse(response); 30 | } 31 | 32 | public String getMessage() { 33 | return message; 34 | } 35 | 36 | public String getProvider() { 37 | return provider; 38 | } 39 | 40 | public String getType() { 41 | return type; 42 | } 43 | 44 | public String getSuccess() { 45 | return Boolean.toString(this.isOk()); 46 | } 47 | 48 | public String getIsPorted() { 49 | return Boolean.toString(this.isPorted); 50 | } 51 | 52 | public void setResponse(String response) { 53 | this.content = response; 54 | JSONObject jsonResponse = new JSONObject(response); 55 | this.parseResponseToObject(jsonResponse); 56 | } 57 | 58 | /** 59 | * Map a Token instance to its Java's Map representation. 60 | * 61 | * @return a Java's Map with the description of this object. 62 | */ 63 | public Map toMap() { 64 | Map map = new HashMap<>(); 65 | 66 | map.put("message", this.getMessage()); 67 | map.put("success", this.getSuccess()); 68 | map.put("is_ported", this.getIsPorted()); 69 | map.put("provider", this.getProvider()); 70 | map.put("type", this.getType()); 71 | 72 | 73 | return map; 74 | } 75 | 76 | private void parseResponseToObject(JSONObject json) { 77 | if (!json.isNull("message")) { 78 | this.message = json.getString("message"); 79 | } 80 | 81 | if (!json.isNull("ported")) { 82 | this.isPorted = json.getBoolean("ported"); 83 | } 84 | 85 | if (!json.isNull("provider")) { 86 | this.provider = json.getString("provider"); 87 | } 88 | 89 | if (!json.isNull("type")) { 90 | this.type = json.getString("type"); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/main/java/com/authy/api/PhoneVerification.java: -------------------------------------------------------------------------------- 1 | package com.authy.api; 2 | 3 | import com.authy.AuthyException; 4 | 5 | /** 6 | * @author Authy Inc 7 | */ 8 | public class PhoneVerification extends Resource { 9 | public static final String PHONE_VERIFICATION_API_PATH = "/protected/json/phones/verification/"; 10 | 11 | public PhoneVerification(String uri, String key) { 12 | super(uri, key, Resource.JSON_CONTENT_TYPE); 13 | } 14 | 15 | public PhoneVerification(String uri, String key, boolean testFlag) { 16 | super(uri, key, testFlag, Resource.JSON_CONTENT_TYPE); 17 | } 18 | 19 | public Verification start(String phoneNumber, String countryCode, String via, Params params) throws AuthyException { 20 | params.setAttribute("phone_number", phoneNumber); 21 | params.setAttribute("country_code", countryCode); 22 | params.setAttribute("via", via); 23 | 24 | final Response response = this.post(PHONE_VERIFICATION_API_PATH + "start", params); 25 | 26 | Verification verification = new Verification(response.getStatus(), response.getBody()); 27 | if (!verification.isOk()) 28 | verification.setError(errorFromJson(response.getBody())); 29 | 30 | return verification; 31 | } 32 | 33 | public Verification check(String phoneNumber, String countryCode, String code) throws AuthyException { 34 | return check(phoneNumber,countryCode, code, new Params()); 35 | } 36 | 37 | public Verification check(String phoneNumber, String countryCode, String code, Params params) throws AuthyException { 38 | params.setAttribute("phone_number", phoneNumber); 39 | params.setAttribute("country_code", countryCode); 40 | params.setAttribute("verification_code", code); 41 | 42 | final Response response = this.get(PHONE_VERIFICATION_API_PATH + "check", params); 43 | 44 | Verification verification = new Verification(response.getStatus(), response.getBody()); 45 | if (!verification.isOk()) 46 | verification.setError(errorFromJson(response.getBody())); 47 | return verification; 48 | 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/authy/api/Resource.java: -------------------------------------------------------------------------------- 1 | package com.authy.api; 2 | 3 | import com.authy.AuthyApiClient; 4 | import com.authy.AuthyException; 5 | import org.json.JSONException; 6 | import org.json.JSONObject; 7 | 8 | import javax.net.ssl.HttpsURLConnection; 9 | import javax.net.ssl.SSLHandshakeException; 10 | import java.io.*; 11 | import java.net.HttpURLConnection; 12 | import java.net.MalformedURLException; 13 | import java.net.URL; 14 | import java.net.URLEncoder; 15 | import java.util.Arrays; 16 | import java.util.HashMap; 17 | import java.util.Map; 18 | import java.util.Map.Entry; 19 | import java.util.Objects; 20 | import java.util.logging.Level; 21 | import java.util.logging.Logger; 22 | 23 | /** 24 | * Class to send http requests. 25 | * 26 | * @author Julian Camargo 27 | */ 28 | public class Resource { 29 | public static class Response { 30 | private final Integer status; 31 | private final String body; 32 | 33 | Response(Integer status, String body) { 34 | this.status = status; 35 | this.body = body; 36 | } 37 | 38 | public Integer getStatus() { 39 | return status; 40 | } 41 | 42 | public String getBody() { 43 | return body; 44 | } 45 | 46 | public String toString() { 47 | return "Response[" + status + ", " + body + "]"; 48 | } 49 | 50 | public boolean equals(Object o) { 51 | return this == o || o instanceof Response && Objects.equals(status, ((Response) o).status) && Objects.equals(body, ((Response) o).body); 52 | } 53 | 54 | public int hashCode() { 55 | return Objects.hash(status, body); 56 | } 57 | } 58 | 59 | private static final Logger LOGGER = Logger.getLogger(Resource.class.getName()); 60 | 61 | public static final String METHOD_POST = "POST"; 62 | public static final String METHOD_GET = "GET"; 63 | public static final String METHOD_PUT = "PUT"; 64 | public static final String ENCODE = "UTF-8"; 65 | public static final String XML_CONTENT_TYPE = "application/xml"; 66 | public static final String JSON_CONTENT_TYPE = "application/json"; 67 | 68 | private final String apiUri; 69 | private final String apiKey; 70 | private final boolean testFlag; 71 | private Map defaultOptions; 72 | private final boolean isJSON; 73 | private final String contentType; 74 | 75 | public Resource(String uri, String key) { 76 | this(uri, key, false, JSON_CONTENT_TYPE); 77 | } 78 | 79 | public Resource(String uri, String key, String contentType) { 80 | this(uri, key, false, contentType); 81 | } 82 | 83 | public Resource(String uri, String key, boolean testFlag) { 84 | this(uri, key, testFlag, JSON_CONTENT_TYPE); 85 | } 86 | 87 | public Resource(String uri, String key, boolean testFlag, String contentType) { 88 | apiUri = uri; 89 | apiKey = key; 90 | this.testFlag = testFlag; 91 | this.contentType = (contentType == null || contentType.equals(XML_CONTENT_TYPE) || !contentType.equals(JSON_CONTENT_TYPE)) ? XML_CONTENT_TYPE : JSON_CONTENT_TYPE; 92 | isJSON = this.contentType.equals(JSON_CONTENT_TYPE); 93 | } 94 | 95 | /** 96 | * POST method. 97 | * 98 | * @param path 99 | * @param data 100 | * @return response from API. 101 | */ 102 | public Response post(String path, Formattable data) throws AuthyException { 103 | return request(Resource.METHOD_POST, path, data, getDefaultOptions()); 104 | } 105 | 106 | /** 107 | * GET method. 108 | * 109 | * @param path 110 | * @param data 111 | * @return response from API. 112 | */ 113 | public Response get(String path, Formattable data) throws AuthyException { 114 | return request(Resource.METHOD_GET, path, data, getDefaultOptions()); 115 | } 116 | 117 | /** 118 | * PUT method. 119 | * 120 | * @param path 121 | * @param data 122 | * @return response from API. 123 | */ 124 | public Response put(String path, Formattable data) throws AuthyException { 125 | return request(Resource.METHOD_PUT, path, data, getDefaultOptions()); 126 | } 127 | 128 | /** 129 | * DELETE method. 130 | * 131 | * @param path 132 | * @param data 133 | * @return response from API. 134 | */ 135 | public Response delete(String path, Formattable data) throws AuthyException { 136 | return request("DELETE", path, data, getDefaultOptions()); 137 | } 138 | 139 | private Response request(String method, String path, Formattable data, Map options) throws AuthyException { 140 | HttpURLConnection connection; 141 | 142 | try { 143 | StringBuilder sb = new StringBuilder(); 144 | 145 | if (method.equals(Resource.METHOD_GET)) { 146 | sb.append(prepareGet(data)); 147 | } 148 | 149 | URL url = new URL(apiUri + path + sb.toString()); 150 | connection = createConnection(url, method, options); 151 | 152 | connection.setRequestProperty("X-Authy-API-Key", apiKey); 153 | 154 | // data might be sent as a null value for cases like "DELETE" requests 155 | if (data!= null && data.toMap().containsKey("api_key")) { 156 | LOGGER.log(Level.WARNING, "Found 'api_key' as a parameter, please remove it, Authy-Java already handles the'api_key' for you."); 157 | } 158 | if (method.equals(Resource.METHOD_POST) || method.equals(Resource.METHOD_PUT)) { 159 | if (isJSON) { 160 | writeJson(connection, data); 161 | } else { 162 | writeXml(connection, data); 163 | } 164 | } 165 | 166 | final int status = connection.getResponseCode(); 167 | return new Response(status, getResponse(connection, status)); 168 | } catch (SSLHandshakeException e) { 169 | throw new AuthyException("SSL verification is failing. Contact support@authy.com", e); 170 | } catch (MalformedURLException e) { 171 | throw new AuthyException("Invalid host", e); 172 | } catch (IOException e) { 173 | throw new AuthyException("Connection error", e); 174 | } 175 | } 176 | 177 | Error errorFromJson(String content) throws AuthyException { 178 | try { 179 | JSONObject errorJson = new JSONObject(content); 180 | Error error = new Error(); 181 | error.setMessage(errorJson.getString("message")); 182 | final int errorCodeNumber = Integer.parseInt(errorJson.getString("error_code")); 183 | final Error.Code error_code = Arrays.stream(Error.Code.values()) 184 | .filter(code -> code.getNumber() == errorCodeNumber) 185 | .findFirst() 186 | .orElse(Error.Code.DEFAULT_ERROR); 187 | error.setCode(error_code); 188 | return error; 189 | } catch (JSONException| NumberFormatException e) { 190 | throw new AuthyException("Invalid response from server", e); 191 | } 192 | } 193 | 194 | public String getContentType() { 195 | return this.contentType; 196 | } 197 | 198 | private HttpURLConnection createConnection(URL url, String method, 199 | Map options) throws IOException { 200 | 201 | 202 | HttpURLConnection connection; 203 | if (testFlag) 204 | connection = (HttpURLConnection) url.openConnection(); 205 | else 206 | connection = (HttpsURLConnection) url.openConnection(); 207 | 208 | connection.setRequestMethod(method); 209 | 210 | for (Entry s : options.entrySet()) { 211 | connection.setRequestProperty(s.getKey(), s.getValue()); 212 | } 213 | 214 | connection.setDoOutput(true); 215 | 216 | return connection; 217 | } 218 | 219 | private String getResponse(HttpURLConnection connection, int status) throws IOException { 220 | InputStream in; 221 | // Load stream 222 | if (status != 200) { 223 | in = connection.getErrorStream(); 224 | } else { 225 | in = connection.getInputStream(); 226 | } 227 | 228 | BufferedInputStream input = new BufferedInputStream(in); 229 | StringBuilder sb = new StringBuilder(); 230 | int ch; 231 | 232 | while ((ch = input.read()) != -1) { 233 | sb.append((char) ch); 234 | } 235 | input.close(); 236 | 237 | return sb.toString(); 238 | } 239 | 240 | private void writeXml(HttpURLConnection connection, Formattable data) throws IOException { 241 | if (data == null) 242 | return; 243 | 244 | OutputStream os = connection.getOutputStream(); 245 | 246 | BufferedWriter output = new BufferedWriter(new OutputStreamWriter(os)); 247 | output.write(data.toXML()); 248 | output.flush(); 249 | output.close(); 250 | } 251 | 252 | private void writeJson(HttpURLConnection connection, Formattable data) throws IOException { 253 | if (data == null) 254 | return; 255 | 256 | OutputStream os = connection.getOutputStream(); 257 | BufferedWriter output = new BufferedWriter(new OutputStreamWriter(os)); 258 | output.write(data.toJSON()); 259 | output.flush(); 260 | output.close(); 261 | } 262 | 263 | 264 | private String prepareGet(Formattable data) { 265 | 266 | if (data == null) 267 | return ""; 268 | 269 | StringBuilder sb = new StringBuilder("?"); 270 | Map params = data.toMap(); 271 | 272 | boolean first = true; 273 | 274 | for (Entry s : params.entrySet()) { 275 | 276 | if (first) { 277 | first = false; 278 | } else { 279 | sb.append('&'); 280 | } 281 | 282 | try { 283 | sb.append(URLEncoder.encode(s.getKey(), ENCODE)).append("=").append(URLEncoder.encode(s.getValue(), ENCODE)); 284 | } catch (UnsupportedEncodingException e) { 285 | System.out.println("Encoding not supported" + e.getMessage()); 286 | } 287 | } 288 | 289 | 290 | return sb.toString(); 291 | } 292 | 293 | private Map getDefaultOptions() { 294 | if (this.defaultOptions == null || this.defaultOptions.isEmpty()) { 295 | this.defaultOptions = new HashMap<>(); 296 | this.defaultOptions.put("Content-Type", contentType); 297 | this.defaultOptions.put("User-Agent", getUserAgent()); 298 | } 299 | return this.defaultOptions; 300 | } 301 | 302 | private String getUserAgent() { 303 | String os = String.format("%s-%s-%s; Java %s", System.getProperty("os.name"), System.getProperty("os.version"), 304 | System.getProperty("os.arch"), System.getProperty("java.specification.version")); 305 | return String.format("%s/%s (%s)", AuthyApiClient.CLIENT_NAME, AuthyApiClient.VERSION, os); 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /src/main/java/com/authy/api/Token.java: -------------------------------------------------------------------------------- 1 | package com.authy.api; 2 | 3 | import javax.xml.bind.annotation.XmlRootElement; 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | 7 | /** 8 | * @author Julian Camargo 9 | */ 10 | @XmlRootElement(name = "token") 11 | public class Token extends Instance implements Formattable { 12 | 13 | public static final String VALID_TOKEN_MESSAGE = "Token is valid."; 14 | 15 | public Token() { 16 | } 17 | 18 | public Token(int status, String content){ 19 | super(status, content); 20 | } 21 | 22 | public Token(int status, String content, String message) { 23 | super(status, content, message); 24 | } 25 | 26 | /** 27 | * Check if this is token is correct. (i.e No error occurred) 28 | * 29 | * @return true if no error occurred else false. 30 | */ 31 | public boolean isOk() { 32 | if (super.isOk()) { 33 | return this.message.equals(VALID_TOKEN_MESSAGE); 34 | } 35 | return false; 36 | } 37 | 38 | /** 39 | * Map a Token instance to its Java's Map representation. 40 | * 41 | * @return a Java's Map with the description of this object. 42 | */ 43 | public Map toMap() { 44 | Map map = new HashMap<>(); 45 | 46 | map.put("status", Integer.toString(status)); 47 | map.put("content", content); 48 | 49 | return map; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/authy/api/Tokens.java: -------------------------------------------------------------------------------- 1 | package com.authy.api; 2 | 3 | import com.authy.AuthyException; 4 | import org.json.JSONException; 5 | import org.json.JSONObject; 6 | import sun.net.www.protocol.http.HttpURLConnection; 7 | 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | 11 | /** 12 | * @author Julian Camargo 13 | */ 14 | public class Tokens extends Resource { 15 | public static final String TOKEN_VERIFICATION_PATH = "/protected/json/verify/"; 16 | 17 | public Tokens(String uri, String key) { 18 | super(uri, key); 19 | } 20 | 21 | public Tokens(String uri, String key, boolean testFlag) { 22 | super(uri, key, testFlag); 23 | } 24 | 25 | public Token verify(int userId, String token) throws AuthyException { 26 | return verify(userId, token, null); 27 | } 28 | 29 | public Token verify(int userId, String token, Map options) throws AuthyException { 30 | InternalToken internalToken = new InternalToken(); 31 | internalToken.setOption(options); 32 | 33 | StringBuilder path = new StringBuilder(TOKEN_VERIFICATION_PATH); 34 | validateToken(token); 35 | path.append(token).append('/'); 36 | path.append(userId); 37 | 38 | final Response response = this.get(path.toString(), internalToken); 39 | return tokenFromJson(response.getStatus(), response.getBody()); 40 | } 41 | 42 | private Token tokenFromJson(int status, String content) throws AuthyException { 43 | if (status == 200) { 44 | try { 45 | JSONObject tokenJSON = new JSONObject(content); 46 | String message = tokenJSON.optString("message"); 47 | return new Token(status, content, message); 48 | 49 | } catch (JSONException e) { 50 | throw new AuthyException("Invalid response from server", e, status); 51 | } 52 | } 53 | 54 | final Error error = errorFromJson(content); 55 | throw new AuthyException("Invalid token", status, error.getCode()); 56 | } 57 | 58 | private void validateToken(String token) throws AuthyException { 59 | int len = token.length(); 60 | if (!isInteger(token)) { 61 | throw new AuthyException("Invalid Token. Only digits accepted.", HttpURLConnection.HTTP_BAD_REQUEST, 62 | Error.Code.TOKEN_INVALID); 63 | } 64 | if (len < 6 || len > 10) { 65 | throw new AuthyException("Invalid Token. Unexpected length.", HttpURLConnection.HTTP_BAD_REQUEST, 66 | Error.Code.TOKEN_INVALID); 67 | } 68 | } 69 | 70 | private boolean isInteger(String s) { 71 | try { 72 | Long.parseLong(s); 73 | } catch (NumberFormatException e) { 74 | return false; 75 | } 76 | return true; 77 | } 78 | 79 | class InternalToken implements Formattable { 80 | Map options; 81 | 82 | InternalToken() { 83 | options = new HashMap<>(); 84 | } 85 | 86 | void setOption(Map options) { 87 | if (options != null) { 88 | this.options = options; 89 | } 90 | } 91 | 92 | public String toXML() { 93 | return null; 94 | } 95 | 96 | public Map toMap() { 97 | if (!options.containsKey("force")) { 98 | options.put("force", "true"); 99 | } 100 | 101 | return options; 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/main/java/com/authy/api/User.java: -------------------------------------------------------------------------------- 1 | package com.authy.api; 2 | 3 | import javax.xml.bind.annotation.XmlElement; 4 | import javax.xml.bind.annotation.XmlRootElement; 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | 8 | /** 9 | * @author Julian Camargo 10 | */ 11 | @XmlRootElement(name = "user") 12 | public class User extends Instance implements Formattable { 13 | int id; 14 | 15 | public User() { 16 | } 17 | 18 | public User(int status, String content) { 19 | super(status, content); 20 | } 21 | 22 | public User(int status, String content, String message) { 23 | super(status, content, message); 24 | } 25 | 26 | @XmlElement(name = "id") 27 | public int getId() { 28 | return id; 29 | } 30 | 31 | public void setId(int id) { 32 | this.id = id; 33 | } 34 | 35 | /** 36 | * Map a Token instance to its Java's Map representation. 37 | * 38 | * @return a Java's Map with the description of this object. 39 | */ 40 | public Map toMap() { 41 | Map map = new HashMap<>(); 42 | 43 | map.put("id", Integer.toString(id)); 44 | map.put("status", Integer.toString(status)); 45 | map.put("content", content); 46 | 47 | return map; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/authy/api/UserStatus.java: -------------------------------------------------------------------------------- 1 | package com.authy.api; 2 | 3 | import javax.xml.bind.annotation.XmlElement; 4 | import javax.xml.bind.annotation.XmlElementWrapper; 5 | import javax.xml.bind.annotation.XmlRootElement; 6 | import java.util.ArrayList; 7 | import java.util.HashMap; 8 | import java.util.List; 9 | import java.util.Map; 10 | 11 | @XmlRootElement(name = "user_status") 12 | public class UserStatus extends Instance implements Formattable { 13 | 14 | @XmlElement(name = "userId") 15 | private int userId; 16 | @XmlElement(name = "success") 17 | private boolean success; 18 | @XmlElement(name = "confirmed") 19 | private boolean confirmed; 20 | @XmlElement(name = "registered") 21 | private boolean registered; 22 | @XmlElement(name = "country_code") 23 | private int countryCode; 24 | @XmlElementWrapper 25 | @XmlElement(name = "device") 26 | private List devices; 27 | @XmlElement(name = "phone_number") 28 | private String phoneNumber; 29 | 30 | public UserStatus() { 31 | super(); 32 | } 33 | 34 | public UserStatus(int status, String content) { 35 | super(status, content); 36 | } 37 | 38 | public UserStatus(int status, String content, String message) { 39 | super(status, content, message); 40 | } 41 | 42 | @Override 43 | public Map toMap() { 44 | Map map = new HashMap<>(); 45 | 46 | map.put("userId", Integer.toString(getUserId())); 47 | map.put("success", Boolean.toString(getSuccess())); 48 | map.put("confirmed", Boolean.toString(isConfirmed())); 49 | map.put("registered", Boolean.toString(isRegistered())); 50 | map.put("countryCode", Integer.toString(getCountryCode())); 51 | map.put("phoneNumber", getPhoneNumber()); 52 | map.put("devices", getDevices().toString()); 53 | 54 | return map; 55 | } 56 | 57 | public int getUserId() { 58 | return userId; 59 | } 60 | 61 | void setUserId(int userId) { 62 | this.userId = userId; 63 | } 64 | 65 | void setMessage(String message) { 66 | this.message = message; 67 | } 68 | 69 | public String getMessage() { 70 | return message; 71 | } 72 | 73 | void setSuccess(boolean success) { 74 | this.success = success; 75 | } 76 | 77 | public boolean getSuccess() { 78 | return success; 79 | } 80 | 81 | public boolean isSuccess() { 82 | return success; 83 | } 84 | 85 | public boolean isConfirmed() { 86 | return confirmed; 87 | } 88 | 89 | void setConfirmed(boolean confirmed) { 90 | this.confirmed = confirmed; 91 | } 92 | 93 | public boolean isRegistered() { 94 | return registered; 95 | } 96 | 97 | void setRegistered(boolean registered) { 98 | this.registered = registered; 99 | } 100 | 101 | public int getCountryCode() { 102 | return countryCode; 103 | } 104 | 105 | void setCountryCode(int countryCode) { 106 | this.countryCode = countryCode; 107 | } 108 | 109 | public List getDevices() { 110 | if (devices == null) { 111 | devices = new ArrayList<>(); 112 | } 113 | return devices; 114 | } 115 | 116 | void setDevices(List devices) { 117 | this.devices = devices; 118 | } 119 | 120 | void setPhoneNumber(String phoneNumber) { 121 | this.phoneNumber = phoneNumber; 122 | } 123 | 124 | public String getPhoneNumber() { 125 | return phoneNumber; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/main/java/com/authy/api/Users.java: -------------------------------------------------------------------------------- 1 | package com.authy.api; 2 | 3 | import com.authy.AuthyException; 4 | 5 | import org.json.JSONArray; 6 | import org.json.JSONException; 7 | import org.json.JSONObject; 8 | 9 | import javax.xml.bind.JAXBContext; 10 | import javax.xml.bind.JAXBException; 11 | import javax.xml.bind.Marshaller; 12 | import javax.xml.bind.annotation.XmlElement; 13 | import javax.xml.bind.annotation.XmlRootElement; 14 | import java.io.StringWriter; 15 | import java.util.HashMap; 16 | import java.util.List; 17 | import java.util.Map; 18 | 19 | /** 20 | * @author Julian Camargo 21 | */ 22 | public class Users extends Resource { 23 | public static final String NEW_USER_PATH = "/protected/json/users/new"; 24 | public static final String DELETE_USER_PATH = "/protected/json/users/delete/"; 25 | public static final String SMS_PATH = "/protected/json/sms/"; 26 | public static final String ONE_CODE_CALL_PATH = "/protected/json/call/"; 27 | public static final String USER_STATUS_PATH = "/protected/json/users/%d/status"; 28 | public static final String DEFAULT_COUNTRY_CODE = "1"; 29 | 30 | public Users(String uri, String key) { 31 | super(uri, key, Resource.JSON_CONTENT_TYPE); 32 | } 33 | 34 | public Users(String uri, String key, boolean testFlag) { 35 | super(uri, key, testFlag, Resource.JSON_CONTENT_TYPE); 36 | } 37 | 38 | /** 39 | * Create a new user using his e-mail, phone and country code. 40 | * 41 | * @param email 42 | * @param phone 43 | * @param countryCode 44 | * @return a User instance 45 | */ 46 | public com.authy.api.User createUser(String email, String phone, String countryCode) throws AuthyException { 47 | User user = new User(email, phone, countryCode); 48 | final Response response = this.post(NEW_USER_PATH, user); 49 | return userFromJson(response.getStatus(), response.getBody()); 50 | } 51 | 52 | /** 53 | * Create a new user using his e-mail and phone. It uses USA country code by default. 54 | * 55 | * @param email 56 | * @param phone 57 | * @return a User instance 58 | */ 59 | public com.authy.api.User createUser(String email, String phone) throws AuthyException { 60 | return createUser(email, phone, DEFAULT_COUNTRY_CODE); 61 | } 62 | 63 | /** 64 | * Send token via sms to a user. 65 | * 66 | * @param userId 67 | * @return Hash instance with API's response. 68 | */ 69 | public Hash requestSms(int userId) throws AuthyException { 70 | return requestSms(userId, new HashMap<>(0)); 71 | } 72 | 73 | /** 74 | * Send token via sms to a user with some options defined. 75 | * 76 | * @param userId 77 | * @param options 78 | * @return Hash instance with API's response. 79 | */ 80 | public Hash requestSms(int userId, Map options) throws AuthyException { 81 | MapToResponse opt = new MapToResponse(options); 82 | final Response response = this.get(SMS_PATH + Integer.toString(userId), opt); 83 | return instanceFromJson(response.getStatus(), response.getBody()); 84 | } 85 | 86 | /** 87 | * Send token via call to a user. 88 | * 89 | * @param userId 90 | * @return Hash instance with API's response. 91 | */ 92 | public Hash requestCall(int userId) throws AuthyException { 93 | return requestCall(userId, new HashMap<>(0)); 94 | } 95 | 96 | /** 97 | * Send token via call to a user with some options defined. 98 | * 99 | * @param userId 100 | * @param options 101 | * @return Hash instance with API's response. 102 | */ 103 | public Hash requestCall(int userId, Map options) throws AuthyException { 104 | MapToResponse opt = new MapToResponse(options); 105 | final Response response = this.get(ONE_CODE_CALL_PATH + Integer.toString(userId), opt); 106 | return instanceFromJson(response.getStatus(), response.getBody()); 107 | } 108 | 109 | /** 110 | * Delete a user. 111 | * 112 | * @param userId 113 | * @return Hash instance with API's response. 114 | */ 115 | public Hash deleteUser(int userId) throws AuthyException { 116 | final Response response = this.post(DELETE_USER_PATH + Integer.toString(userId), null); 117 | return instanceFromJson(response.getStatus(), response.getBody()); 118 | } 119 | 120 | /** 121 | * Get user status. 122 | * 123 | * @return object containing user status 124 | */ 125 | public UserStatus requestStatus(int userId) throws AuthyException { 126 | final Response response = this.get(String.format(USER_STATUS_PATH, userId), null); 127 | UserStatus userStatus = userStatusFromJson(response); 128 | return userStatus; 129 | } 130 | 131 | private com.authy.api.User userFromJson(int status, String content) throws AuthyException { 132 | com.authy.api.User user = new com.authy.api.User(status, content); 133 | if (user.isOk()) { 134 | JSONObject userJson = new JSONObject(content); 135 | user.setId(userJson.getJSONObject("user").getInt("id")); 136 | } else { 137 | Error error = errorFromJson(content); 138 | user.setError(error); 139 | } 140 | return user; 141 | } 142 | 143 | private Hash instanceFromJson(int status, String content) throws AuthyException { 144 | Hash hash = new Hash(status, content); 145 | if (hash.isOk()) { 146 | try { 147 | JSONObject jsonResponse = new JSONObject(content); 148 | String message = jsonResponse.optString("message"); 149 | hash.setMessage(message); 150 | 151 | boolean success = jsonResponse.optBoolean("success"); 152 | hash.setSuccess(success); 153 | 154 | String token = jsonResponse.optString("token"); 155 | hash.setToken(token); 156 | } catch (JSONException e) { 157 | throw new AuthyException("Invalid response from server", e); 158 | } 159 | } else { 160 | Error error = errorFromJson(content); 161 | hash.setError(error); 162 | } 163 | 164 | return hash; 165 | } 166 | 167 | private UserStatus userStatusFromJson(Response response) throws AuthyException { 168 | UserStatus userStatus = new UserStatus(response.getStatus(), response.getBody()); 169 | if (userStatus.isOk()) { 170 | try { 171 | JSONObject jsonResponse = new JSONObject(response.getBody()); 172 | String message = jsonResponse.optString("message"); 173 | userStatus.setMessage(message); 174 | 175 | boolean success = jsonResponse.optBoolean("success"); 176 | userStatus.setSuccess(success); 177 | 178 | JSONObject status = jsonResponse.getJSONObject("status"); 179 | int userId = status.getInt("authy_id"); 180 | userStatus.setUserId(userId); 181 | 182 | boolean confirmed = status.getBoolean("confirmed"); 183 | userStatus.setConfirmed(confirmed); 184 | 185 | boolean registered = status.getBoolean("registered"); 186 | userStatus.setRegistered(registered); 187 | 188 | int countryCode = status.getInt("country_code"); 189 | userStatus.setCountryCode(countryCode); 190 | 191 | String phoneNumber = status.getString("phone_number"); 192 | userStatus.setPhoneNumber(phoneNumber); 193 | 194 | JSONArray devicesArray = status.getJSONArray("devices"); 195 | List devices = userStatus.getDevices(); 196 | for (int i = 0; i < devicesArray.length(); i++) { 197 | devices.add(devicesArray.getString(i)); 198 | } 199 | 200 | } catch (JSONException e) { 201 | throw new AuthyException("Invalid response from server", e); 202 | } 203 | } else { 204 | Error error = errorFromJson(response.getBody()); 205 | userStatus.setError(error); 206 | } 207 | 208 | return userStatus; 209 | } 210 | 211 | static class MapToResponse implements Formattable { 212 | private Map options; 213 | 214 | MapToResponse(Map options) { 215 | this.options = options; 216 | } 217 | 218 | public String toXML() { 219 | return ""; 220 | } 221 | 222 | public Map toMap() { 223 | return options; 224 | } 225 | } 226 | 227 | @XmlRootElement(name = "user") 228 | static class User implements Formattable { 229 | String email, cellphone, countryCode; 230 | 231 | public User() { 232 | } 233 | 234 | public User(String email, String cellphone, String countryCode) { 235 | this.email = email; 236 | this.cellphone = cellphone; 237 | this.countryCode = countryCode; 238 | } 239 | 240 | @XmlElement(name = "email") 241 | public String getEmail() { 242 | return email; 243 | } 244 | 245 | public void setEmail(String email) { 246 | this.email = email; 247 | } 248 | 249 | @XmlElement(name = "cellphone") 250 | public String getCellphone() { 251 | return cellphone; 252 | } 253 | 254 | public void setCellphone(String cellphone) { 255 | this.cellphone = cellphone; 256 | } 257 | 258 | @XmlElement(name = "country_code") 259 | public String getCountryCode() { 260 | return countryCode; 261 | } 262 | 263 | public void setCountryCode(String countryCode) { 264 | this.countryCode = countryCode; 265 | } 266 | 267 | public String toXML() { 268 | StringWriter sw = new StringWriter(); 269 | String xml = ""; 270 | 271 | try { 272 | JAXBContext context = JAXBContext.newInstance(this.getClass()); 273 | Marshaller marshaller = context.createMarshaller(); 274 | 275 | marshaller.marshal(this, sw); 276 | 277 | xml = sw.toString(); 278 | } catch (JAXBException e) { 279 | e.printStackTrace(); 280 | } 281 | return xml; 282 | } 283 | 284 | public Map toMap() { 285 | 286 | Map map = new HashMap<>(); 287 | map.put("email", email); 288 | map.put("cellphone", cellphone); 289 | map.put("country_code", countryCode); 290 | 291 | return map; 292 | } 293 | 294 | @Override 295 | public String toJSON() { 296 | JSONObject json = new JSONObject(); 297 | json.put("user", toMap()); 298 | return json.toString(); 299 | } 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /src/main/java/com/authy/api/Verification.java: -------------------------------------------------------------------------------- 1 | package com.authy.api; 2 | 3 | import org.json.JSONObject; 4 | 5 | import javax.xml.bind.annotation.XmlElement; 6 | import javax.xml.bind.annotation.XmlRootElement; 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | 10 | /** 11 | * @author Moisés Vargas 12 | */ 13 | @XmlRootElement(name = "verification") 14 | public class Verification extends Instance implements Formattable { 15 | private boolean isPorted = false; 16 | private boolean isCellphone = false; 17 | 18 | public Verification() { 19 | } 20 | 21 | public Verification(int status, String response) { 22 | this(status, response, null); 23 | } 24 | 25 | public Verification(int status, String response, String message) { 26 | super(status, response, message); 27 | this.setResponse(response); 28 | } 29 | 30 | @XmlElement(name = "message") 31 | public String getMessage() { 32 | return message; 33 | } 34 | 35 | @XmlElement(name = "success") 36 | public String getSuccess() { 37 | return Boolean.toString(this.isOk()); 38 | } 39 | 40 | @XmlElement(name = "is_ported") 41 | public String getIsPorted() { 42 | return Boolean.toString(this.isPorted); 43 | } 44 | 45 | @XmlElement(name = "is_cellphone") 46 | public String getIsCellphone() { 47 | return Boolean.toString(this.isCellphone); 48 | } 49 | 50 | public void setStatus(int status) { 51 | this.status = status; 52 | } 53 | 54 | public void setResponse(String response) { 55 | this.content = response; 56 | JSONObject jsonResponse = new JSONObject(response); 57 | this.parseResponseToOjbect(jsonResponse); 58 | } 59 | 60 | /** 61 | * Map a Token instance to its Java's Map representation. 62 | * 63 | * @return a Java's Map with the description of this object. 64 | */ 65 | public Map toMap() { 66 | Map map = new HashMap<>(); 67 | 68 | map.put("message", this.getMessage()); 69 | map.put("success", this.getSuccess()); 70 | map.put("is_ported", this.getIsPorted()); 71 | map.put("is_cellphone", this.getIsCellphone()); 72 | 73 | return map; 74 | } 75 | 76 | private void parseResponseToOjbect(JSONObject json) { 77 | if (!json.isNull("message")) 78 | this.message = json.getString("message"); 79 | 80 | if (!json.isNull("is_ported")) 81 | this.isPorted = json.getBoolean("is_ported"); 82 | 83 | if (!json.isNull("is_cellphone")) 84 | this.isCellphone = json.getBoolean("is_cellphone"); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/test/java/com/authy/TestAuthyUtil.java: -------------------------------------------------------------------------------- 1 | package com.authy; 2 | 3 | import org.junit.Test; 4 | 5 | import java.io.UnsupportedEncodingException; 6 | import java.net.URLDecoder; 7 | import java.util.AbstractMap.SimpleImmutableEntry; 8 | import java.util.Arrays; 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | 12 | import static java.util.stream.Collectors.toMap; 13 | import static org.hamcrest.core.Is.is; 14 | import static org.junit.Assert.assertThat; 15 | 16 | /** 17 | * Unit tests for {@link com.authy.AuthyUtil} 18 | * 19 | * @author hansospina 20 | *

21 | * Copyright © 2016 Twilio, Inc. All Rights Reserved. 22 | */ 23 | public class TestAuthyUtil { 24 | private final String testApikey = "FU10H0uCafgKnXvPfDm5iAisVuHDgXJA"; 25 | private final String testCallbackUrl = "https://requestb.in/ui9vdiui"; 26 | 27 | 28 | @Test 29 | public void testValidSignatureApprovedPost() throws UnsupportedEncodingException, OneTouchException { 30 | final String testNonce = "1512923419"; 31 | final String expectedSignature = "6uKAaKxCkkFEyQujwdudyPNUgRi2fY5otoCOQN7VjCw="; 32 | final String callbackBody = "{\"authy_id\":688611,\"device_uuid\":2102943,\"callback_action\":\"approval_request_status\",\"uuid\":\"4a725520-bff5-0135-eb01-0e5d90336a8c\",\"status\":\"approved\",\"approval_request\":{\"transaction\":{\"details\":null,\"device_details\":{},\"device_geolocation\":\"\",\"device_signing_time\":1512923419,\"encrypted\":false,\"flagged\":false,\"hidden_details\":null,\"message\":\"Authorize OneTouch Unit Test\",\"reason\":\"\",\"requester_details\":null,\"status\":\"approved\",\"uuid\":\"4a725520-bff5-0135-eb01-0e5d90336a8c\",\"created_at_time\":1512923400,\"customer_uuid\":15083},\"expiration_timestamp\":1513009800,\"logos\":null,\"app_id\":\"582380eb7e1caa03317ec08c\"},\"signature\":\"pQmypvnmoIb7qIHrKcd3nwPArh8Ecr8L87XTYqqAfagDkXhOD7CulSdDkE0ImFBzigwc+vxwZsgxBnhFmU2c9kYuvMJiPLeS8NCpRucg7eHeGPM0jQbKveqzFcZ9L6P1kRHjSYwS7dLqEINBffNckK7O9LHz13XYklxXvYUwvemtj+yEemyCJbmlJbCSlUTyajr3WSRPMYZV7xXTNtWp2XvcRSclP8izgA1cV/cw7ctDYIPG6wUXJGSIs/kg3hTDeN1Z3YBq1fnMkfxeb5g9bRveRlCjXpQ6xFh1wQEUbbtJpf+uRgxddbQwxfda9gIb5osOEhKRv5HoJ03yOvKiBQ==\",\"device\":{\"city\":null,\"country\":\"Colombia\",\"ip\":\"190.130.65.251\",\"region\":null,\"registration_city\":null,\"registration_country\":\"Colombia\",\"registration_ip\":\"190.130.66.242\",\"registration_method\":\"sms\",\"registration_region\":null,\"os_type\":\"android\",\"last_account_recovery_at\":null,\"id\":2102943,\"registration_date\":1505418165,\"last_sync_date\":1505418173}}"; 33 | 34 | Map headers = createHeaders(testNonce, expectedSignature); 35 | final boolean valid = AuthyUtil.validateSignatureForPost(callbackBody, headers, testCallbackUrl, testApikey); 36 | 37 | assertThat(valid, is(true)); 38 | } 39 | 40 | @Test 41 | public void testValidSignatureApprovedWithUnlockMethodPost() throws UnsupportedEncodingException, OneTouchException { 42 | final String testNonce = "1570641874"; 43 | final String expectedSignature = "QZL9dnpVH5bjPMRvC98pm9zsQ2Zv1PToXlpN5X8H1hI="; 44 | final String callbackBody = "{\"authy_id\":688611,\"device_uuid\":2102943,\"callback_action\":\"approval_request_status\",\"uuid\":\"4a725520-bff5-0135-eb01-0e5d90336a8c\",\"status\":\"approved\",\"approval_request\":{\"transaction\":{\"details\":{\"Username\":\"jdoe\",\"Location\":\"earth\",\"Process ID\":\"1234567890\"},\"device_details\":{},\"device_geolocation\":\"\",\"device_signing_time\":1512923419,\"encrypted\":false,\"flagged\":false,\"hidden_details\":{\"transaction_id\":\"TR139872562346\"},\"message\":\"Authorize OneTouch Unit Test\",\"reason\":\"\",\"requester_details\":null,\"status\":\"approved\",\"uuid\":\"4a725520-bff5-0135-eb01-0e5d90336a8c\",\"created_at_time\":1512923400,\"customer_uuid\":15083},\"expiration_timestamp\":1513009800,\"logos\":null,\"app_id\":\"582380eb7e1caa03317ec08c\"},\"signature\":\"pQmypvnmoIb7qIHrKcd3nwPArh8Ecr8L87XTYqqAfagDkXhOD7CulSdDkE0ImFBzigwc+vxwZsgxBnhFmU2c9kYuvMJiPLeS8NCpRucg7eHeGPM0jQbKveqzFcZ9L6P1kRHjSYwS7dLqEINBffNckK7O9LHz13XYklxXvYUwvemtj+yEemyCJbmlJbCSlUTyajr3WSRPMYZV7xXTNtWp2XvcRSclP8izgA1cV/cw7ctDYIPG6wUXJGSIs/kg3hTDeN1Z3YBq1fnMkfxeb5g9bRveRlCjXpQ6xFh1wQEUbbtJpf+uRgxddbQwxfda9gIb5osOEhKRv5HoJ03yOvKiBQ==\",\"device\":{\"city\":null,\"country\":\"Colombia\",\"enabled_unlock_methods\":[\"pin\",\"fingerprint\"],\"ip\":\"190.130.65.251\",\"last_unlock_method_used\":\"pin\",\"region\":null,\"registration_city\":null,\"registration_country\":\"Colombia\",\"registration_ip\":\"190.130.66.242\",\"registration_method\":\"sms\",\"registration_region\":null,\"os_type\":\"android\",\"last_account_recovery_at\":null,\"multidevice_enabled\":true,\"multidevice_updated_at\":1570469080,\"id\":2102943,\"registration_date\":1505418165,\"last_sync_date\":1505418173,\"last_unlock_date\":1570641596}}"; 45 | 46 | Map headers = createHeaders(testNonce, expectedSignature); 47 | final boolean valid = AuthyUtil.validateSignatureForPost(callbackBody, headers, testCallbackUrl, testApikey); 48 | 49 | assertThat(valid, is(true)); 50 | } 51 | 52 | @Test 53 | public void testValidSignatureDeniedWithDetailsPost() throws UnsupportedEncodingException, OneTouchException { 54 | final String testNonce = "1512940231"; 55 | final String expectedSignature = "HFc5mICOVRrEmpmBoIuubKWcN0YYd50TaO7YQJFrnlM="; 56 | final String callbackBody = "{\"authy_id\":688611,\"device_uuid\":2102943,\"callback_action\":\"approval_request_status\",\"uuid\":\"6edb8f40-c01c-0135-c648-0e00ace7f69c\",\"status\":\"denied\",\"approval_request\":{\"transaction\":{\"details\":{\"username\":\"User\",\"location\":\"California,USA\",\"cosa1\":\"cosa1\",\"cosa2\":\"cosa2\"},\"device_details\":{},\"device_geolocation\":\"\",\"device_signing_time\":1512940232,\"encrypted\":false,\"flagged\":false,\"hidden_details\":{\"ip_address\":\"10.10.3.203\"},\"message\":\"Authorize OneTouch Unit Test\",\"reason\":\"\",\"requester_details\":null,\"status\":\"denied\",\"uuid\":\"6edb8f40-c01c-0135-c648-0e00ace7f69c\",\"created_at_time\":1512940211,\"customer_uuid\":15083},\"expiration_timestamp\":1513026611,\"logos\":null,\"app_id\":\"582380eb7e1caa03317ec08c\"},\"signature\":\"jTLeK9tHUAhJRexjBIq3DVowPRKE+8578YzJD5yizXqYqmeQT7t8NS1uFfbmjHKabgIvC0N/WEFfyvKWjARrNVR6FJ5EjbOZJy7ouQT+9iTaorsJDVPUPeVnQUUTi3noXcSonGN0+YW7foHf8zMnTTyQBbjurexv2dkfu0fLdiF8I6xRhMeq5sf5APdZCt7NsFIM95N0wO6MHoD5sLL8yFrB1C/RB35n6BIxgTWz0TjtbcO+V/rqjgMK47xTITPsbEo46ammhRl4vU5flcM2O6KE6Q7tKLCftAzs/3xu13w1KKkFXmCqXpeB29lSNU2wveGI7nB2eIk41medDJ81Dg==\",\"device\":{\"city\":null,\"country\":\"Colombia\",\"ip\":\"190.130.65.251\",\"region\":null,\"registration_city\":null,\"registration_country\":\"Colombia\",\"registration_ip\":\"190.130.66.242\",\"registration_method\":\"sms\",\"registration_region\":null,\"os_type\":\"android\",\"last_account_recovery_at\":null,\"id\":2102943,\"registration_date\":1505418165,\"last_sync_date\":1505418173}}"; 57 | 58 | Map headers = createHeaders(testNonce, expectedSignature); 59 | final boolean valid = AuthyUtil.validateSignatureForPost(callbackBody, headers, testCallbackUrl, testApikey); 60 | 61 | assertThat(valid, is(true)); 62 | } 63 | 64 | @Test 65 | public void testValidSignatureApprovedWithDetailsAndLogosPost() throws UnsupportedEncodingException, OneTouchException { 66 | final String testNonce = "1512940917"; 67 | final String expectedSignature = "IFD2P2f5w2Jis1uref9Blu7a0liztOsnso16Gh6y054="; 68 | final String callbackBody = "{\"authy_id\":688611,\"device_uuid\":2102943,\"callback_action\":\"approval_request_status\",\"uuid\":\"098c3580-c01e-0135-eb00-0e5d90336a8c\",\"status\":\"approved\",\"approval_request\":{\"transaction\":{\"details\":{\"username\":\"User\",\"location\":\"California,USA\",\"cosa1\":\"cosa1\",\"cosa2\":\"cosa2\"},\"device_details\":{},\"device_geolocation\":\"\",\"device_signing_time\":1512940918,\"encrypted\":false,\"flagged\":false,\"hidden_details\":{\"ip_address\":\"10.10.3.203\"},\"message\":\"Authorize OneTouch Unit Test\",\"reason\":\"\",\"requester_details\":null,\"status\":\"approved\",\"uuid\":\"098c3580-c01e-0135-eb00-0e5d90336a8c\",\"created_at_time\":1512940900,\"customer_uuid\":15083},\"expiration_timestamp\":1513027300,\"logos\":[{\"res\":\"default\",\"url\":\"https://www.itsalllost.com/wp-content/uploads/2017/04/twilio-logo-red.png\"},{\"res\":\"med\",\"url\":\"https://www.itsalllost.com/wp-content/uploads/2017/04/twilio-logo-red.png\"}],\"app_id\":\"582380eb7e1caa03317ec08c\"},\"signature\":\"qOLMuHVy4KITm2nSd1PxJCv+ydjcduKwxz2Fc7pMrDm7QtU2hMAnY5AUxdwlae5WJmEWNM8OctdGhMJweTwICkkOgYm2v+u7k/wz5zuPozDDnMqJWBjiCfbKNpKqf8CQ2dBndtxi2Sl7/y57KiXYJfTlGHNBhCoTxVzVBNDEPu6OLV6KA60mcEW87tg1b4Q/p69ZkYb5B1f9Ujk/ueCbCK6JDhtUf1v3/baNgO8o/mp2EydiFughqpiHIIOR0VY9/o/hh5a7z6FG4OxZ3WmS7q2506Wy698LW3ZRNl0aXwtFIat4IyCSSuDQFW9LZEcXdQK4YixSJ8b5H8vlRErSPQ==\",\"device\":{\"city\":null,\"country\":\"Colombia\",\"ip\":\"190.130.65.251\",\"region\":null,\"registration_city\":null,\"registration_country\":\"Colombia\",\"registration_ip\":\"190.130.66.242\",\"registration_method\":\"sms\",\"registration_region\":null,\"os_type\":\"android\",\"last_account_recovery_at\":null,\"id\":2102943,\"registration_date\":1505418165,\"last_sync_date\":1505418173}}"; 69 | 70 | Map headers = createHeaders(testNonce, expectedSignature); 71 | final boolean valid = AuthyUtil.validateSignatureForPost(callbackBody, headers, testCallbackUrl, testApikey); 72 | 73 | assertThat(valid, is(true)); 74 | } 75 | 76 | @Test 77 | public void testValidSignatureApprovedGet() throws UnsupportedEncodingException, OneTouchException { 78 | final String testNonce = "1512937258"; 79 | final String expectedSignature = "W0YumwSPoL+2CX1q4XoUSY5gOmWmnrkXUmlm7NE6iGY="; 80 | final String callbackQueryString = "approval_request%5Bapp_id%5D=582380eb7e1caa03317ec08c&approval_request%5Bexpiration_timestamp%5D=1513023630&approval_request%5Blogos%5D=&approval_request%5Btransaction%5D%5Bcreated_at_time%5D=1512937230&approval_request%5Btransaction%5D%5Bcustomer_uuid%5D=15083&approval_request%5Btransaction%5D%5Bdetails%5D=&approval_request%5Btransaction%5D%5Bdevice_geolocation%5D=&approval_request%5Btransaction%5D%5Bdevice_signing_time%5D=1512937257&approval_request%5Btransaction%5D%5Bencrypted%5D=false&approval_request%5Btransaction%5D%5Bflagged%5D=false&approval_request%5Btransaction%5D%5Bhidden_details%5D=&approval_request%5Btransaction%5D%5Bmessage%5D=Authorize+OneTouch+Unit+Test&approval_request%5Btransaction%5D%5Breason%5D=&approval_request%5Btransaction%5D%5Brequester_details%5D=&approval_request%5Btransaction%5D%5Bstatus%5D=approved&approval_request%5Btransaction%5D%5Buuid%5D=7e272da0-c015-0135-eafc-0e5d90336a8c&authy_id=688611&callback_action=approval_request_status&device%5Bcity%5D=&device%5Bcountry%5D=Colombia&device%5Bid%5D=2102943&device%5Bip%5D=190.130.65.251&device%5Blast_account_recovery_at%5D=&device%5Blast_sync_date%5D=1505418173&device%5Bos_type%5D=android&device%5Bregion%5D=&device%5Bregistration_city%5D=&device%5Bregistration_country%5D=Colombia&device%5Bregistration_date%5D=1505418165&device%5Bregistration_ip%5D=190.130.66.242&device%5Bregistration_method%5D=sms&device%5Bregistration_region%5D=&device_uuid=2102943&signature=YnZe2qSWEAYAROAhykwbeIOV2Ym%2Fg4y9rlIQc6ePtvTt9UDDotl7p2H%2FpC3EFNG5XsDaMkJuZmXSd0UW%2FtiuR2l%2BJ%2Fvta5yXVArq6d1uNspB1u%2BWYjumDhTLFQI0Ox6BGQMhTlWkQK96dh0bJQqyPP7I5f4xQZMmNIClCKZa%2BzUqmPA7zo1Qokz9w0u917zKt%2BsLLxLLXblhdYvFIvfVqGAtiBQzbUh9UCCuOp6jcF7HkZiGs2nFIAlwVFffhK%2BxXnEXvG9yA3KnMRX6Y31yxWd3ApRoDNbLpCOSgFlkYAasQ8hBkcSy3AyyfT4TMhyeemI3IW6GFTADrXXO%2BzvpcA%3D%3D&status=approved&uuid=7e272da0-c015-0135-eafc-0e5d90336a8c"; 81 | 82 | Map headers = createHeaders(testNonce, expectedSignature); 83 | Map params = extractQueryParams(callbackQueryString); 84 | final boolean valid = AuthyUtil.validateSignatureForGet(params, headers, testCallbackUrl, testApikey); 85 | 86 | assertThat(valid, is(true)); 87 | } 88 | 89 | @Test 90 | public void testValidSignatureApprovedWithDetailsGet() throws UnsupportedEncodingException, OneTouchException { 91 | final String testNonce = "1512942246"; 92 | final String expectedSignature = "oCSbVsRS7uwKH1bRAnyem3pz9rTBSMpcEaS6W33DqmM="; 93 | final String callbackQueryString = "approval_request%5Bapp_id%5D=582380eb7e1caa03317ec08c&approval_request%5Bexpiration_timestamp%5D=1513028630&approval_request%5Blogos%5D=&approval_request%5Btransaction%5D%5Bcreated_at_time%5D=1512942230&approval_request%5Btransaction%5D%5Bcustomer_uuid%5D=15083&approval_request%5Btransaction%5D%5Bdetails%5D%5Bcosa1%5D=cosa1&approval_request%5Btransaction%5D%5Bdetails%5D%5Bcosa2%5D=cosa2&approval_request%5Btransaction%5D%5Bdetails%5D%5Blocation%5D=California%2CUSA&approval_request%5Btransaction%5D%5Bdetails%5D%5Busername%5D=User&approval_request%5Btransaction%5D%5Bdevice_geolocation%5D=&approval_request%5Btransaction%5D%5Bdevice_signing_time%5D=1512942247&approval_request%5Btransaction%5D%5Bencrypted%5D=false&approval_request%5Btransaction%5D%5Bflagged%5D=false&approval_request%5Btransaction%5D%5Bhidden_details%5D%5Bip_address%5D=10.10.3.203&approval_request%5Btransaction%5D%5Bmessage%5D=Authorize+OneTouch+Unit+Test&approval_request%5Btransaction%5D%5Breason%5D=&approval_request%5Btransaction%5D%5Brequester_details%5D=&approval_request%5Btransaction%5D%5Bstatus%5D=approved&approval_request%5Btransaction%5D%5Buuid%5D=224fdf40-c021-0135-fa62-06ca50569adc&authy_id=688611&callback_action=approval_request_status&device%5Bcity%5D=&device%5Bcountry%5D=Colombia&device%5Bid%5D=2102943&device%5Bip%5D=190.130.65.251&device%5Blast_account_recovery_at%5D=&device%5Blast_sync_date%5D=1505418173&device%5Bos_type%5D=android&device%5Bregion%5D=&device%5Bregistration_city%5D=&device%5Bregistration_country%5D=Colombia&device%5Bregistration_date%5D=1505418165&device%5Bregistration_ip%5D=190.130.66.242&device%5Bregistration_method%5D=sms&device%5Bregistration_region%5D=&device_uuid=2102943&signature=rRf5hjOsPql%2Fumb%2FzI6azWUx2Xx8s6hhKeOYXob0tqhIA7WUtUcvXDNfn%2BX%2FnAqOiBDcGr41aNU6mFw1nCbQI3jwtm9n%2F7RVtxrJN%2B85370dY0nOUpl19IGJ6xwyRa0U76svfePBROnrobGdyCvtHw6G4tT%2BJ3oo2T7Ji1TZ4scFLaRfiA95VmFYgJd6tEqoBom%2FtX8itKyxa%2FFTVSc8OriSusyX8GpxqSAKl9GjVKGp7W0p%2FGTzTU9lzVAIPHsIyX9%2FH%2BKUGk5d7oglcPb%2B0HX2V1ruSIg5IId5j3yrBPd%2Bjxf3HLTcrPMGjFZJhapMOwE4fgEWG9p6pfBYOyyauw%3D%3D&status=approved&uuid=224fdf40-c021-0135-fa62-06ca50569adc"; 94 | 95 | Map headers = createHeaders(testNonce, expectedSignature); 96 | Map params = extractQueryParams(callbackQueryString); 97 | final boolean valid = AuthyUtil.validateSignatureForGet(params, headers, testCallbackUrl, testApikey); 98 | 99 | assertThat(valid, is(true)); 100 | } 101 | 102 | @Test 103 | public void testValidSignatureApprovedWithDetailsAndLogosGet() throws UnsupportedEncodingException, OneTouchException { 104 | final String testNonce = "1512943081"; 105 | final String expectedSignature = "5TM5uf+8WlPMjkREHeS39XUHv8CHjhq3/u/+/lWU7cg="; 106 | final String callbackQueryString = "approval_request%5Bapp_id%5D=582380eb7e1caa03317ec08c&approval_request%5Bexpiration_timestamp%5D=1513029464&approval_request%5Blogos%5D%5B%5D%5Bres%5D=default&approval_request%5Blogos%5D%5B%5D%5Burl%5D=https%3A%2F%2Fwww.itsalllost.com%2Fwp-content%2Fuploads%2F2017%2F04%2Ftwilio-logo-red.png&approval_request%5Btransaction%5D%5Bcreated_at_time%5D=1512943064&approval_request%5Btransaction%5D%5Bcustomer_uuid%5D=15083&approval_request%5Btransaction%5D%5Bdetails%5D%5Bcosa1%5D=cosa1&approval_request%5Btransaction%5D%5Bdetails%5D%5Bcosa2%5D=cosa2&approval_request%5Btransaction%5D%5Bdetails%5D%5Blocation%5D=California%2CUSA&approval_request%5Btransaction%5D%5Bdetails%5D%5Busername%5D=User&approval_request%5Btransaction%5D%5Bdevice_geolocation%5D=&approval_request%5Btransaction%5D%5Bdevice_signing_time%5D=1512943081&approval_request%5Btransaction%5D%5Bencrypted%5D=false&approval_request%5Btransaction%5D%5Bflagged%5D=false&approval_request%5Btransaction%5D%5Bhidden_details%5D%5Bip_address%5D=10.10.3.203&approval_request%5Btransaction%5D%5Bmessage%5D=Authorize+OneTouch+Unit+Test&approval_request%5Btransaction%5D%5Breason%5D=&approval_request%5Btransaction%5D%5Brequester_details%5D=&approval_request%5Btransaction%5D%5Bstatus%5D=approved&approval_request%5Btransaction%5D%5Buuid%5D=133c7590-c023-0135-fa61-06ca50569adc&authy_id=688611&callback_action=approval_request_status&device%5Bcity%5D=&device%5Bcountry%5D=Colombia&device%5Bid%5D=2102943&device%5Bip%5D=190.130.65.251&device%5Blast_account_recovery_at%5D=&device%5Blast_sync_date%5D=1505418173&device%5Bos_type%5D=android&device%5Bregion%5D=&device%5Bregistration_city%5D=&device%5Bregistration_country%5D=Colombia&device%5Bregistration_date%5D=1505418165&device%5Bregistration_ip%5D=190.130.66.242&device%5Bregistration_method%5D=sms&device%5Bregistration_region%5D=&device_uuid=2102943&signature=Q%2FTaHEbdfmBBw%2BT%2BgVBrM8Sw%2BdqT%2FnhdO6BJNc%2F7herUg4BxAwziQhdmQhqjY2nvtVJkWEa9PdB11tNjTcbMQs8cchyPPNLUvp8L7C82snxcH%2Fgdde65Z%2B39Aug5hCcXUoQ92PsRckvexkrCQASDWbvmJZnjVM5t3j%2BKXn2QVyZkc63GBE1W9GPNM7bmlL2ZOENPoxpUq9%2B%2FawTEb8WMDebDcFEPGSgNFcvk6%2FJTSwjzmmm8Ypk82s4P1lUq74WWxaDM7XJH2Q4mo9vHkDaRIweEx%2BliMqUQYoCtApD0vsPSe4Exfp5tfcelWE9xrte%2FF3qKiPSnV8xS0SY9L20%2F7A%3D%3D&status=approved&uuid=133c7590-c023-0135-fa61-06ca50569adc"; 107 | 108 | Map headers = createHeaders(testNonce, expectedSignature); 109 | Map params = extractQueryParams(callbackQueryString); 110 | final boolean valid = AuthyUtil.validateSignatureForGet(params, headers, testCallbackUrl, testApikey); 111 | 112 | assertThat(valid, is(true)); 113 | } 114 | 115 | private Map extractQueryParams(String callbackQueryString) { 116 | return Arrays.stream(callbackQueryString.split("&")).map(it -> { 117 | final int idx = it.indexOf("="); 118 | final String key = URLDecoder.decode(idx > 0 ? it.substring(0, idx) : it); 119 | final String value = URLDecoder.decode(idx > 0 && it.length() > idx + 1 ? it.substring(idx + 1) : ""); 120 | return new SimpleImmutableEntry<>(key, value); 121 | }).collect(toMap(SimpleImmutableEntry::getKey, SimpleImmutableEntry::getValue)); 122 | } 123 | 124 | private Map createHeaders(String nonce, String signature) { 125 | Map headers = new HashMap<>(); 126 | headers.put(AuthyUtil.HEADER_AUTHY_SIGNATURE_NONCE, nonce); 127 | headers.put(AuthyUtil.HEADER_AUTHY_SIGNATURE, signature); 128 | return headers; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/test/java/com/authy/api/TestApiBase.java: -------------------------------------------------------------------------------- 1 | package com.authy.api; 2 | 3 | import com.github.tomakehurst.wiremock.junit.WireMockRule; 4 | import org.junit.Rule; 5 | 6 | public class TestApiBase { 7 | @Rule 8 | public WireMockRule wireMockRule = new WireMockRule(18089); 9 | 10 | protected final String testHost = "http://localhost:18089"; 11 | protected final String testApiKey = "test_api_key"; 12 | } 13 | -------------------------------------------------------------------------------- /src/test/java/com/authy/api/TestOneTouch.java: -------------------------------------------------------------------------------- 1 | package com.authy.api; 2 | 3 | import static com.authy.api.Error.Code.ONETOUCH_APPROVAL_REQUEST_NOT_FOUND; 4 | import static com.authy.api.Error.Code.USER_NOT_FOUND; 5 | import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; 6 | import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; 7 | import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; 8 | import static com.github.tomakehurst.wiremock.client.WireMock.get; 9 | import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; 10 | import static com.github.tomakehurst.wiremock.client.WireMock.post; 11 | import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; 12 | import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; 13 | import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; 14 | import static com.github.tomakehurst.wiremock.client.WireMock.verify; 15 | 16 | import static junit.framework.TestCase.assertEquals; 17 | import static junit.framework.TestCase.assertFalse; 18 | 19 | import com.authy.AuthyApiClient; 20 | import com.authy.AuthyException; 21 | import com.authy.OneTouchException; 22 | import com.authy.api.ApprovalRequestParams.Resolution; 23 | 24 | import org.junit.Assert; 25 | import org.junit.Before; 26 | import org.junit.Rule; 27 | import org.junit.Test; 28 | import org.junit.rules.ExpectedException; 29 | 30 | /** 31 | * Unit tests for the API described at: http://docs.authy.com/onetouch.html#onetouch-api 32 | * 33 | * @author hansospina 34 | *

35 | * Copyright © 2017 Twilio, Inc. All Rights Reserved. 36 | */ 37 | public class TestOneTouch extends TestApiBase { 38 | 39 | final private String testUserId = "30144611"; 40 | final private String testOneTouchUUID = "8d031630-d15b-0134-b0a8-0a77bbe8093e"; 41 | final private String successStatusResponse = "{" + 42 | " \"approval_request\": {" + 43 | " \"_app_name\": \"testhans\"," + 44 | " \"_app_serial_id\": 46113," + 45 | " \"_authy_id\": 28894864," + 46 | " \"_id\": \"589d12e07d558e6b91e9370a\"," + 47 | " \"_user_email\": \"hans+1@allcode.com\"," + 48 | " \"app_id\": \"58547f6f4014250a11c201f6\"," + 49 | " \"created_at\": \"2017-02-10T01:09:52Z\"," + 50 | " \"notified\": false," + 51 | " \"processed_at\": \"2017-02-13T11:11:58Z\"," + 52 | " \"seconds_to_expire\": 86400," + 53 | " \"status\": \"expired\"," + 54 | " \"updated_at\": \"2017-02-13T11:11:58Z\"," + 55 | " \"user_id\": \"5840a9328aa2fb674c485436\"," + 56 | " \"uuid\": \"8d031630-d15b-0134-b0a8-0a77bbe8093e\"" + 57 | " }," + 58 | " \"success\": true" + 59 | "}"; 60 | 61 | final private String successSendApprovalRequestResponse = "{" + 62 | " \"approval_request\": {" + 63 | " \"uuid\": \"dda2c400-bc43-0135-d7be-1285ca17e122\"" + 64 | " }," + 65 | " \"success\": true" + 66 | "}"; 67 | 68 | final private String userNotFoundResponse = "{" 69 | + " \"message\": \"User not found.\"," 70 | + " \"success\": false," 71 | + " \"errors\": {" 72 | + " \"message\": \"User not found.\"" 73 | + " }," 74 | + " \"error_code\": \"60026\"" 75 | + "}"; 76 | 77 | final private String approvalRequestNotFound = "{" 78 | + " \"message\": \"Approval request not found: 3f05a350-4b2c-0136-f779-12c0c2bf9easd\"," 79 | + " \"success\": false," 80 | + " \"errors\": {}," 81 | + " \"error_code\": \"60049\"" 82 | + "}"; 83 | 84 | @Rule 85 | public ExpectedException thrown = ExpectedException.none(); 86 | private AuthyApiClient client; 87 | 88 | @Before 89 | public void setUp() { 90 | client = new AuthyApiClient(testApiKey, testHost, true); 91 | } 92 | 93 | @Test 94 | public void testEmptyMessage() throws OneTouchException { 95 | thrown.expect(OneTouchException.class); 96 | thrown.expectMessage(ApprovalRequestParams.Builder.MESSAGE_ERROR); 97 | 98 | ApprovalRequestParams approvalRequestParams = new ApprovalRequestParams.Builder(Integer.parseInt(testUserId), "") 99 | .addDetail("username", "User") 100 | .addDetail("location", "California,USA") 101 | .addHiddenDetail("ip_address", "10.10.3.203") 102 | .addLogo(Resolution.Default, "http://image.co").build(); 103 | 104 | Assert.fail(); 105 | } 106 | 107 | @Test 108 | public void testAuthNull() throws OneTouchException { 109 | thrown.expect(OneTouchException.class); 110 | thrown.expectMessage(ApprovalRequestParams.Builder.AUTHYID_ERROR); 111 | 112 | ApprovalRequestParams approvalRequestParams = new ApprovalRequestParams.Builder(null, "Authorize OneTouch Unit Test") 113 | .addDetail("username", "User") 114 | .addDetail("location", "California,USA") 115 | .addHiddenDetail("ip_address", "10.10.3.203") 116 | .addLogo(Resolution.Default, "http://image.co") 117 | .build(); 118 | 119 | Assert.fail(); 120 | } 121 | 122 | @Test 123 | public void testSendApprovalRequestOk() throws AuthyException { 124 | stubFor(post(urlPathEqualTo("/onetouch/json/users/" + testUserId + "/approval_requests")) 125 | .willReturn(aResponse() 126 | .withStatus(200) 127 | .withHeader("Content-Type", "application/json;charset=utf-8") 128 | .withBody(successSendApprovalRequestResponse))); 129 | 130 | ApprovalRequestParams approvalRequestParams = new ApprovalRequestParams.Builder(Integer.parseInt(testUserId), "Authorize OneTouch Unit Test") 131 | .addDetail("username", "User") 132 | .addDetail("location", "California,USA") 133 | .addHiddenDetail("ip_address", "10.10.3.203") 134 | .addLogo(Resolution.Low, "http://image.low") 135 | .addLogo(Resolution.Medium, "http://image.co") 136 | .addLogo(Resolution.Default, "http://image.co") 137 | .build(); 138 | 139 | OneTouchResponse response = client.getOneTouch().sendApprovalRequest(approvalRequestParams); 140 | 141 | verify(postRequestedFor(urlPathEqualTo("/onetouch/json/users/" + testUserId + "/approval_requests")) 142 | .withHeader("X-Authy-API-Key", equalTo(testApiKey)) 143 | .withRequestBody(equalToJson("{" + 144 | " \"hidden_details\" : {" + 145 | " \"ip_address\" : \"10.10.3.203\"" + 146 | " }," + 147 | " \"details\" : {" + 148 | " \"location\" : \"California,USA\"," + 149 | " \"username\" : \"User\"" + 150 | " }," + 151 | " \"message\" : \"Authorize OneTouch Unit Test\"," + 152 | " \"logos\" : [ {" + 153 | " \"res\" : \"med\"," + 154 | " \"url\" : \"http://image.co\"" + 155 | " }, {" + 156 | " \"res\" : \"low\"," + 157 | " \"url\" : \"http://image.low\"" + 158 | " }, {" + 159 | " \"res\" : \"default\"," + 160 | " \"url\" : \"http://image.co\"" + 161 | " } ]" + 162 | "}", true, true))); 163 | Assert.assertTrue(response.isSuccess()); 164 | } 165 | 166 | @Test 167 | public void testBadDetail() throws OneTouchException { 168 | thrown.expect(OneTouchException.class); 169 | thrown.expectMessage(ApprovalRequestParams.Builder.DETAIL_ERROR); 170 | 171 | ApprovalRequestParams approvalRequestParams = new ApprovalRequestParams.Builder(Integer.parseInt(testUserId), "Authorize OneTouch Unit Test") 172 | .addDetail("", "") 173 | .addHiddenDetail("ip_address", "10.10.3.203") 174 | .addLogo(Resolution.Default, "http://image.co") 175 | .build(); 176 | 177 | Assert.fail(); 178 | } 179 | 180 | @Test 181 | public void testHiddenDetail() throws OneTouchException { 182 | thrown.expect(OneTouchException.class); 183 | thrown.expectMessage(ApprovalRequestParams.Builder.HIDDEN_DETAIL_ERROR); 184 | 185 | ApprovalRequestParams approvalRequestParams = new ApprovalRequestParams.Builder(Integer.parseInt(testUserId), "Authorize OneTouch Unit Test") 186 | .addDetail("username", "User") 187 | .addDetail("location", "California,USA") 188 | .addHiddenDetail("ip_address", null) 189 | .addLogo(Resolution.Default, "http://image.co") 190 | .build(); 191 | 192 | Assert.fail(); 193 | } 194 | 195 | @Test 196 | public void testDefaultLogo() throws OneTouchException { 197 | thrown.expect(OneTouchException.class); 198 | thrown.expectMessage(ApprovalRequestParams.Builder.LOGO_ERROR_DEFAULT); 199 | 200 | ApprovalRequestParams approvalRequestParams = new ApprovalRequestParams.Builder(Integer.parseInt(testUserId), "Authorize OneTouch Unit Test") 201 | .addDetail("username", "User") 202 | .addDetail("location", "California,USA") 203 | .addLogo(Resolution.Low, "http://image.co") 204 | .build(); 205 | 206 | Assert.fail(); 207 | } 208 | 209 | 210 | @Test 211 | public void testGetApprovalRequestStatus() throws Exception { 212 | stubFor(get(urlPathEqualTo("/onetouch/json/approval_requests/" + testOneTouchUUID)) 213 | .willReturn(aResponse() 214 | .withStatus(200) 215 | .withHeader("Content-Type", "application/json;charset=utf-8") 216 | .withBody(successStatusResponse))); 217 | 218 | OneTouchResponse response = client.getOneTouch().getApprovalRequestStatus(testOneTouchUUID); 219 | 220 | verify(getRequestedFor(urlPathEqualTo("/onetouch/json/approval_requests/" + testOneTouchUUID)) 221 | .withHeader("X-Authy-API-Key", equalTo(testApiKey))); 222 | Assert.assertTrue(response.isSuccess()); 223 | Assert.assertNotNull(response.getApprovalRequest().getStatus()); 224 | } 225 | 226 | @Test 227 | public void testUserNotFoundError() throws Exception { 228 | String invalidUserId = "12342325"; 229 | stubFor(post(urlPathEqualTo("/onetouch/json/users/" + invalidUserId + "/approval_requests")) 230 | .willReturn(aResponse() 231 | .withStatus(404) 232 | .withHeader("Content-Type", "application/json;charset=utf-8") 233 | .withBody(userNotFoundResponse))); 234 | 235 | ApprovalRequestParams approvalRequestParams = new ApprovalRequestParams.Builder(Integer.parseInt(invalidUserId), "Authorize OneTouch Unit Test") 236 | .addDetail("username", "User") 237 | .addDetail("location", "California,USA") 238 | .addHiddenDetail("ip_address", "10.10.3.203") 239 | .addLogo(Resolution.Low, "http://image.low") 240 | .addLogo(Resolution.Medium, "http://image.co") 241 | .addLogo(Resolution.Default, "http://image.co") 242 | .build(); 243 | 244 | OneTouchResponse response = client.getOneTouch().sendApprovalRequest(approvalRequestParams); 245 | 246 | assertFalse(response.isSuccess()); 247 | assertEquals(USER_NOT_FOUND, response.getError().getCode()); 248 | } 249 | 250 | @Test 251 | public void testApprovalRequestNotFoundError() throws Exception { 252 | String invalidOneTouchUUID = "3f05a350-4b2c-0136-f779-12c0c2bf9easd"; 253 | stubFor(get(urlPathEqualTo("/onetouch/json/approval_requests/" + invalidOneTouchUUID)) 254 | .willReturn(aResponse() 255 | .withStatus(404) 256 | .withHeader("Content-Type", "application/json;charset=utf-8") 257 | .withBody(approvalRequestNotFound))); 258 | 259 | OneTouchResponse response = client.getOneTouch().getApprovalRequestStatus(invalidOneTouchUUID); 260 | 261 | assertFalse(response.isSuccess()); 262 | assertEquals(ONETOUCH_APPROVAL_REQUEST_NOT_FOUND, response.getError().getCode()); 263 | assertEquals("60049", response.getErrorCode()); 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /src/test/java/com/authy/api/TestPhoneInfo.java: -------------------------------------------------------------------------------- 1 | package com.authy.api; 2 | 3 | import static com.authy.api.Error.Code.PHONE_INFO_ERROR_QUERYING; 4 | import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; 5 | import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; 6 | import static com.github.tomakehurst.wiremock.client.WireMock.get; 7 | import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; 8 | import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; 9 | import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; 10 | import static com.github.tomakehurst.wiremock.client.WireMock.verify; 11 | 12 | import static junit.framework.TestCase.assertEquals; 13 | 14 | import com.authy.AuthyException; 15 | 16 | import org.junit.Test; 17 | 18 | public class TestPhoneInfo extends TestApiBase { 19 | private static final String successResponse = "{" + 20 | " \"message\": \"Phone number information as of 2017-11-25 23:21:39 UTC\"," + 21 | " \"type\": \"voip\"," + 22 | " \"provider\": \"Pinger\"," + 23 | " \"ported\": false," + 24 | " \"success\": true" + 25 | "}"; 26 | 27 | final private String phoneInfoError = "{" + 28 | " \"error_code\": \"60025\"," + 29 | " \"message\": \"Server error while querying phone information. Please try again later\"," + 30 | " \"errors\": {" + 31 | " \"message\": \"Server error while querying phone information. Please try again later\"" + 32 | " }," + 33 | " \"success\": false" + 34 | "}"; 35 | 36 | @Test 37 | public void testPhoneInfo() throws AuthyException { 38 | stubFor(get(urlPathEqualTo("/protected/json/phones/info")) 39 | .willReturn(aResponse() 40 | .withStatus(200) 41 | .withHeader("Content-Type", "application/json;charset=utf-8") 42 | .withBody(successResponse))); 43 | final PhoneInfo client = new PhoneInfo(testHost, testApiKey, true); 44 | 45 | final String phoneNumber = "7754615609"; 46 | final String countryCode = "1"; 47 | final PhoneInfoResponse result = client.info(phoneNumber, countryCode); 48 | 49 | verify(getRequestedFor(urlPathEqualTo("/protected/json/phones/info")) 50 | .withHeader("X-Authy-API-Key", equalTo(testApiKey)) 51 | .withQueryParam("phone_number", equalTo(phoneNumber)) 52 | .withQueryParam("country_code", equalTo(countryCode))); 53 | assertEquals(true, result.getMessage().contains("Phone number information as of")); 54 | assertEquals("Pinger", result.getProvider()); 55 | assertEquals("voip", result.getType()); 56 | assertEquals("false", result.getIsPorted()); 57 | assertEquals("true", result.getSuccess()); 58 | } 59 | 60 | @Test 61 | public void testPhoneInfoError() throws AuthyException { 62 | stubFor(get(urlPathEqualTo("/protected/json/phones/info")) 63 | .willReturn(aResponse() 64 | .withStatus(500) 65 | .withHeader("Content-Type", "application/json") 66 | .withBody(phoneInfoError))); 67 | final PhoneInfo client = new PhoneInfo(testHost, testApiKey, true); 68 | 69 | final String phoneNumber = "7754615609"; 70 | final String countryCode = "1"; 71 | final PhoneInfoResponse result = client.info(phoneNumber, countryCode); 72 | 73 | assertEquals(true, result.getMessage().contains("Server error while querying phone information")); 74 | assertEquals("false", result.getSuccess()); 75 | assertEquals(PHONE_INFO_ERROR_QUERYING, result.getError().getCode()); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/test/java/com/authy/api/TestPhoneVerification.java: -------------------------------------------------------------------------------- 1 | package com.authy.api; 2 | 3 | import com.authy.AuthyException; 4 | import org.junit.Before; 5 | import org.junit.Test; 6 | 7 | import static com.authy.api.Error.Code.INVALID_PHONE_NUMBER; 8 | import static com.authy.api.Error.Code.PHONE_VERIFICATION_INCORRECT; 9 | import static com.github.tomakehurst.wiremock.client.WireMock.*; 10 | import static org.junit.Assert.*; 11 | 12 | public class TestPhoneVerification extends TestApiBase { 13 | 14 | private PhoneVerification client; 15 | 16 | private String getStartSuccessResponse(final String message){ 17 | return "{" + 18 | " \"carrier\": \"Pinger - Bandwidth.com - Sybase365\"," + 19 | " \"is_cellphone\": false," + 20 | " \"message\": \""+ message + "\"," + 21 | " \"seconds_to_expire\": 599," + 22 | " \"uuid\": \"bec828c0-b535-0135-8e26-1226b57fac04\"," + 23 | " \"success\": true" + 24 | "}"; 25 | } 26 | 27 | final private String startInvalidNumberResponse = "{" + 28 | " \"error_code\": \"60033\"," + 29 | " \"message\": \"Phone number is invalid\"," + 30 | " \"errors\": {" + 31 | " \"message\": \"Phone number is invalid\"" + 32 | " }," + 33 | " \"success\": false" + 34 | "}"; 35 | 36 | final private String checkIncorrectVerificationResponse = "{" + 37 | " \"error_code\": \"60022\"," + 38 | " \"message\": \"Verification code is incorrect\"," + 39 | " \"errors\": {" + 40 | " \"message\": \"Verification code is incorrect\"" + 41 | " }," + 42 | " \"success\": false" + 43 | "}"; 44 | 45 | final private String checkCorrectVerificationResponse = "{" + 46 | " \"message\": \"Verification code is correct.\"," + 47 | " \"success\": true" + 48 | "}"; 49 | 50 | @Before 51 | public void setUp() { 52 | client = new PhoneVerification(testHost, testApiKey, true); 53 | } 54 | 55 | @Test 56 | public void testContentTypeToBeJson() { 57 | assertEquals("application/json", client.getContentType()); 58 | } 59 | 60 | @Test 61 | public void testVerificationStartEs() throws AuthyException { 62 | stubFor(post(urlPathEqualTo("/protected/json/phones/verification/start")) 63 | .willReturn(aResponse() 64 | .withStatus(200) 65 | .withHeader("Content-Type", "application/json;charset=utf-8") 66 | .withBody(getStartSuccessResponse("Llamada a +1 775-461-5609 fue iniciada.")))); 67 | 68 | Params params = new Params(); 69 | params.setAttribute("locale", "es"); 70 | Verification result = client.start("775-461-5609", "1", "call", params); 71 | 72 | verify(postRequestedFor(urlPathEqualTo("/protected/json/phones/verification/start")) 73 | .withHeader("X-Authy-API-Key", equalTo(testApiKey)) 74 | .withRequestBody(equalToJson("{\"country_code\": \"1\", \"phone_number\": \"775-461-5609\", \"locale\": \"es\", \"via\": \"call\"}", true, true))); 75 | assertEquals("Llamada a +1 775-461-5609 fue iniciada.", result.getMessage()); 76 | assertEquals("true", result.getSuccess()); 77 | } 78 | 79 | @Test 80 | public void testVerificationStartEn() throws AuthyException { 81 | stubFor(post(urlPathEqualTo("/protected/json/phones/verification/start")) 82 | .willReturn(aResponse() 83 | .withStatus(200) 84 | .withHeader("Content-Type", "application/json;charset=utf-8") 85 | .withBody(getStartSuccessResponse("Text message sent to +1 775-461-5609.")))); 86 | 87 | Params params = new Params(); 88 | params.setAttribute("locale", "en"); 89 | Verification result = client.start("775-461-5609", "1", "sms", params); 90 | 91 | verify(postRequestedFor(urlPathEqualTo("/protected/json/phones/verification/start")) 92 | .withHeader("X-Authy-API-Key", equalTo(testApiKey)) 93 | .withRequestBody(equalToJson("{\"country_code\": \"1\", \"phone_number\": \"775-461-5609\", \"locale\": \"en\", \"via\": \"sms\"}", true, true))); 94 | String msg = "Text message sent to +1 775-461-5609."; 95 | assertEquals(msg, result.getMessage()); 96 | assertEquals("true", result.getSuccess()); 97 | } 98 | 99 | @Test 100 | public void testVerificationStartEnInvalid() throws AuthyException { 101 | stubFor(post(urlPathEqualTo("/protected/json/phones/verification/start")) 102 | .willReturn(aResponse() 103 | .withStatus(400) 104 | .withHeader("Content-Type", "application/json") 105 | .withBody(startInvalidNumberResponse))); 106 | 107 | Params params = new Params(); 108 | params.setAttribute("locale", "en"); 109 | 110 | Verification result = client.start("282-23", "1", "sms", params); 111 | 112 | assertEquals("Phone number is invalid", result.getMessage()); 113 | assertEquals("false", result.getSuccess()); 114 | Error error = result.getError(); 115 | assertNotNull(error); 116 | assertEquals(INVALID_PHONE_NUMBER, error.getCode()); 117 | } 118 | 119 | @Test 120 | public void testVerificationCheckSuccess() throws AuthyException { 121 | stubFor(get(urlPathEqualTo("/protected/json/phones/verification/check")) 122 | .willReturn(aResponse() 123 | .withStatus(200) 124 | .withHeader("Content-Type", "application/json") 125 | .withBody(checkCorrectVerificationResponse))); 126 | 127 | Verification result = client.check("775-461-5609", "1", "2061"); 128 | 129 | assertEquals("Verification code is correct.", result.getMessage()); 130 | assertTrue(result.isOk()); 131 | assertEquals("true", result.getSuccess()); 132 | } 133 | 134 | @Test 135 | public void testVerificationCheckIncorrectCode() throws AuthyException { 136 | stubFor(get(urlPathEqualTo("/protected/json/phones/verification/check")) 137 | .willReturn(aResponse() 138 | .withStatus(401) 139 | .withHeader("Content-Type", "application/json") 140 | .withBody(checkIncorrectVerificationResponse))); 141 | 142 | Verification result = client.check("775-461-5609", "1", "2061"); 143 | 144 | assertEquals("Verification code is incorrect", result.getMessage()); 145 | assertFalse(result.isOk()); 146 | assertEquals("false", result.getSuccess()); 147 | Error error = result.getError(); 148 | assertNotNull(error); 149 | assertEquals(PHONE_VERIFICATION_INCORRECT, error.getCode()); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/test/java/com/authy/api/TestUsers.java: -------------------------------------------------------------------------------- 1 | package com.authy.api; 2 | 3 | import com.authy.AuthyApiClient; 4 | import com.authy.AuthyException; 5 | 6 | import org.junit.Before; 7 | import org.junit.Test; 8 | 9 | import java.util.HashMap; 10 | 11 | import static com.authy.api.Error.Code.USER_NOT_FOUND; 12 | import static com.authy.api.Error.Code.USER_NOT_VALID; 13 | import static com.github.tomakehurst.wiremock.client.WireMock.*; 14 | 15 | import static org.hamcrest.CoreMatchers.containsString; 16 | import static org.hamcrest.core.Is.is; 17 | import static org.junit.Assert.*; 18 | 19 | public class TestUsers extends TestApiBase { 20 | 21 | private Users client; 22 | final private String testUserId = "30144611"; 23 | 24 | private final String successResponseForced = "{" + 25 | " \"success\": true," + 26 | " \"message\": \"SMS token was sent\"," + 27 | " \"cellphone\": \"+57-XXX-XXX-XX12\"" + 28 | "}"; 29 | 30 | private final String userNotFoundResponse = "{" + 31 | " \"message\": \"User not found.\"," + 32 | " \"success\": false," + 33 | " \"errors\": {" + 34 | " \"message\": \"User not found.\"" + 35 | " }," + 36 | " \"error_code\": \"60026\"" + 37 | "}"; 38 | 39 | private final String successResponseNotForced = "{" + 40 | " \"message\": \"Ignored: SMS is not needed for smartphones. Pass force=true if you want to actually send it anyway.\"," + 41 | " \"cellphone\": \"+57-XXX-XXX-XX12\"," + 42 | " \"device\": \"android\"," + 43 | " \"ignored\": true," + 44 | " \"success\": true" + 45 | "}"; 46 | 47 | private final String successCreateUserResponse = "{\"message\":\"User created successfully.\",\"user\":{\"id\":1000},\"success\":true}"; 48 | 49 | @Before 50 | public void setUp() { 51 | client = new AuthyApiClient(testApiKey, testHost, true).getUsers(); 52 | } 53 | 54 | @Test 55 | public void testCreateUser() throws AuthyException { 56 | stubFor(post(urlPathEqualTo("/protected/json/users/new")) 57 | .willReturn(aResponse() 58 | .withStatus(200) 59 | .withHeader("Content-Type", "application/json") 60 | .withBody(successCreateUserResponse))); 61 | 62 | final User user = client.createUser("test@example.com", "3003003333", "57"); 63 | 64 | verify(postRequestedFor(urlPathEqualTo("/protected/json/users/new")) 65 | .withHeader("X-Authy-API-Key", equalTo(testApiKey)) 66 | .withHeader("Content-Type", equalTo("application/json")) 67 | .withRequestBody(equalToJson("{" + 68 | "\"user\": {" 69 | + " \"country_code\" : \"57\"," 70 | + " \"cellphone\" : \"3003003333\"," 71 | + " \"email\" : \"test@example.com\"" 72 | + "}}"))); 73 | assertEquals(1000, user.getId()); 74 | assertTrue(user.isOk()); 75 | } 76 | 77 | @Test 78 | public void testCreateUserDefaultCountry() throws AuthyException { 79 | stubFor(post(urlPathEqualTo("/protected/json/users/new")) 80 | .willReturn(aResponse() 81 | .withStatus(200) 82 | .withHeader("Content-Type", "application/json") 83 | .withBody(successCreateUserResponse))); 84 | 85 | final User user = client.createUser("test@example.com", "3003003333"); 86 | 87 | verify(postRequestedFor(urlPathEqualTo("/protected/json/users/new")) 88 | .withHeader("X-Authy-API-Key", equalTo(testApiKey)) 89 | .withHeader("Content-Type", equalTo("application/json")) 90 | .withRequestBody(equalToJson("{" + 91 | "\"user\": {" 92 | + " \"country_code\" : \"1\"," 93 | + " \"cellphone\" : \"3003003333\"," 94 | + " \"email\" : \"test@example.com\"" 95 | + "}}"))); 96 | assertEquals(1000, user.getId()); 97 | assertTrue(user.isOk()); 98 | } 99 | 100 | @Test 101 | public void testCreateUserErrorInvalid() throws AuthyException { 102 | stubFor(post(urlPathEqualTo("/protected/json/users/new")) 103 | .willReturn(aResponse() 104 | .withStatus(400) 105 | .withHeader("Content-Type", "application/json") 106 | .withBody("{" + 107 | " \"message\": \"User was not valid\"," + 108 | " \"success\": false," + 109 | " \"errors\": {" + 110 | " \"cellphone\": \"is invalid\"," + 111 | " \"message\": \"User was not valid\"" + 112 | " }," + 113 | " \"cellphone\": \"is invalid\"," + 114 | " \"error_code\": \"60027\"" + 115 | "}"))); 116 | 117 | final User user = client.createUser("test@example.com", "3001"); //Invalid Phone sent 118 | 119 | assertFalse(user.isOk()); 120 | assertEquals(400, user.getStatus()); 121 | final Error error = user.getError(); 122 | assertEquals("User was not valid", error.getMessage()); 123 | assertEquals(USER_NOT_VALID, error.getCode()); 124 | } 125 | 126 | @Test 127 | public void testRequestSMS() throws AuthyException { 128 | stubFor(get(urlPathEqualTo("/protected/json/sms/" + testUserId)) 129 | .willReturn(aResponse() 130 | .withStatus(200) 131 | .withHeader("Content-Type", "application/json") 132 | .withBody(successResponseForced))); 133 | 134 | // let's setup some extra parameters 135 | HashMap map = new HashMap<>(); 136 | // We are testing SMS here so let's add the force param to have Authy send the SMS even if the 137 | // user has the Authy App installed 138 | map.put("force", "true"); 139 | // This is the API normal call you will do to send an SMS, if we don't pass the force option authy will be 140 | // smart enough to decide if it sends the sms or just notifies the user inside the app 141 | Hash response = client.requestSms(Integer.parseInt(testUserId), map); 142 | 143 | verify(getRequestedFor(urlPathEqualTo("/protected/json/sms/" + testUserId)) 144 | .withHeader("X-Authy-API-Key", equalTo(testApiKey)) 145 | .withQueryParam("force", equalTo("true"))); 146 | // isOK() is the method that will allow you to know if the request worked. 147 | assertTrue(response.isOk()); 148 | // there's also a response message. 149 | assertEquals("SMS token was sent", response.getMessage()); 150 | } 151 | 152 | @Test 153 | public void testUserNotFoundSMS() throws AuthyException { 154 | final Integer badUserId = 0; 155 | stubFor(get(urlPathEqualTo("/protected/json/sms/" + badUserId)) 156 | .willReturn(aResponse() 157 | .withStatus(404) 158 | .withHeader("Content-Type", "application/json") 159 | .withBody(userNotFoundResponse))); 160 | 161 | // let's setup some extra parameters 162 | HashMap map = new HashMap<>(); 163 | // We are testing SMS here so let's add the force param to have Authy send the SMS even if the 164 | // user has the Authy App installed 165 | map.put("force", "true"); 166 | // This is the API normal call you will do to send an SMS, if we don't pass the force option authy will be 167 | // smart enough to decide if it sends the sms or just notifies the user inside the app 168 | Hash reponse = client.requestSms(badUserId, map); 169 | 170 | verify(getRequestedFor(urlPathEqualTo("/protected/json/sms/" + badUserId)) 171 | .withHeader("X-Authy-API-Key", equalTo(testApiKey)) 172 | .withQueryParam("force", equalTo("true"))); 173 | // isOK() is the method that will allow you to know if the request worked. 174 | assertFalse(reponse.isOk()); 175 | final Error error = reponse.getError(); 176 | assertNotNull(error); 177 | assertEquals(USER_NOT_FOUND, error.getCode()); 178 | } 179 | 180 | @Test 181 | public void testRequestSMSNoForce() throws AuthyException { 182 | stubFor(get(urlPathEqualTo("/protected/json/sms/" + testUserId)) 183 | .willReturn(aResponse() 184 | .withStatus(200) 185 | .withHeader("Content-Type", "application/json") 186 | .withBody(successResponseNotForced))); 187 | 188 | // This is the API normal call you will do to send an SMS, if we don't pass the force option authy will be 189 | // smart enough to decide if it sends the sms or just notifies the user inside the app 190 | Hash response = client.requestSms(Integer.parseInt(testUserId)); 191 | 192 | verify(getRequestedFor(urlPathEqualTo("/protected/json/sms/" + testUserId)) 193 | .withHeader("X-Authy-API-Key", equalTo(testApiKey))); 194 | // isOK() is the method that will allow you to know if the request worked. 195 | assertTrue(response.isOk()); 196 | // there's also a response message. 197 | assertEquals("Ignored: SMS is not needed for smartphones. Pass force=true if you want to actually send it anyway.", response.getMessage()); 198 | } 199 | 200 | @Test 201 | public void testRequestCall() throws AuthyException { 202 | stubFor(get(urlPathEqualTo("/protected/json/call/" + testUserId)) 203 | .willReturn(aResponse() 204 | .withStatus(200) 205 | .withHeader("Content-Type", "application/json") 206 | .withBody("{" + 207 | " \"message\": \"Call ignored. User is using App Tokens and this call is not necessary. Pass force=true if you still want to call users that are using the App.\"," 208 | + 209 | " \"cellphone\": \"+57-XXX-XXX-XX12\"," + 210 | " \"device\": \"android\"," + 211 | " \"ignored\": true," + 212 | " \"success\": true" + 213 | "}"))); 214 | 215 | Hash response = client.requestCall(Integer.parseInt(testUserId)); 216 | 217 | verify(getRequestedFor(urlPathEqualTo("/protected/json/call/" + testUserId)) 218 | .withHeader("X-Authy-API-Key", equalTo(testApiKey))); 219 | assertTrue(response.isOk()); 220 | assertThat(response.getMessage(), containsString("Call ignored. User is using App Tokens and this call is not necessary.")); 221 | } 222 | 223 | @Test 224 | public void testRemoveUser() throws AuthyException { 225 | stubFor(post(urlPathEqualTo("/protected/json/users/delete/" + testUserId)) 226 | .willReturn(aResponse() 227 | .withStatus(200) 228 | .withHeader("Content-Type", "application/json") 229 | .withBody("{" + 230 | " \"message\": \"User removed from application\"," + 231 | " \"success\": true" + 232 | "}"))); 233 | 234 | Hash response = client.deleteUser(Integer.parseInt(testUserId)); 235 | 236 | verify(postRequestedFor(urlPathEqualTo("/protected/json/users/delete/" + testUserId)) 237 | .withHeader("Content-Type", equalTo("application/json")) //TODO: this Content-Type even if it works doesn't seem like the more appropriate 238 | .withHeader("X-Authy-API-Key", equalTo(testApiKey)) 239 | .withRequestBody(equalTo(""))); 240 | assertTrue(response.isOk()); 241 | assertThat(response.getMessage(), containsString("User removed from application")); 242 | } 243 | 244 | @Test 245 | public void testRemoveUserErrorNotFound() throws AuthyException { 246 | stubFor(post(urlPathEqualTo("/protected/json/users/delete/" + testUserId)) 247 | .willReturn(aResponse() 248 | .withStatus(404) 249 | .withHeader("Content-Type", "application/json") 250 | .withBody(userNotFoundResponse))); 251 | 252 | Hash response = client.deleteUser(Integer.parseInt(testUserId)); 253 | 254 | assertFalse(response.isOk()); 255 | assertThat(response.getStatus(), is(404)); 256 | final Error error = response.getError(); 257 | assertThat(error.getMessage(), containsString("User not found")); 258 | assertEquals(USER_NOT_FOUND, error.getCode()); 259 | } 260 | 261 | @Test 262 | public void testUserToXML() throws Exception { 263 | Users.User user = new Users.User("fake@email.com", "12345678", "1"); 264 | String xml = user.toXML(); 265 | 266 | assertEquals(xml, "123456781fake@email.com"); 267 | } 268 | 269 | @Test 270 | public void testRequestUserStatus() throws AuthyException { 271 | stubFor(get(urlPathEqualTo("/protected/json/users/" + testUserId + "/status")) 272 | .willReturn(aResponse() 273 | .withStatus(200) 274 | .withHeader("Content-Type", "application/json") 275 | .withBody("{" 276 | + " \"status\": {" 277 | + " \"authy_id\":2," 278 | + " \"confirmed\":true," 279 | + " \"registered\":true," 280 | + " \"country_code\":1," 281 | + " \"phone_number\":\"XXX-XXX-9302\"," 282 | + " \"devices\": [" 283 | + " \"authy_chrome\"," 284 | + " \"android\"" 285 | + " ]" 286 | + " }," 287 | + " \"message\":\"User status.\"," 288 | + " \"success\":true" 289 | + "}"))); 290 | 291 | final UserStatus userStatus = client.requestStatus(Integer.parseInt(testUserId)); 292 | assertNotNull(userStatus); 293 | assertEquals(2, userStatus.getUserId()); 294 | assertEquals("User status.", userStatus.getMessage()); 295 | assertEquals(1, userStatus.getCountryCode()); 296 | assertEquals("XXX-XXX-9302", userStatus.getPhoneNumber()); 297 | assertTrue(userStatus.isConfirmed()); 298 | assertTrue(userStatus.isRegistered()); 299 | assertEquals(2, userStatus.getDevices().size()); 300 | assertEquals("authy_chrome", userStatus.getDevices().get(0)); 301 | assertEquals("android", userStatus.getDevices().get(1)); 302 | } 303 | 304 | @Test 305 | public void testRequestUserStatusNotFound() throws AuthyException { 306 | stubFor(get(urlPathEqualTo("/protected/json/users/" + testUserId + "/status")) 307 | .willReturn(aResponse() 308 | .withStatus(404) 309 | .withHeader("Content-Type", "application/json") 310 | .withBody(userNotFoundResponse))); 311 | 312 | final UserStatus userStatus = client.requestStatus(Integer.parseInt(testUserId)); 313 | assertNotNull(userStatus); 314 | assertFalse(userStatus.isOk()); 315 | assertThat(userStatus.getStatus(), is(404)); 316 | final Error error = userStatus.getError(); 317 | assertThat(error.getMessage(), containsString("User not found")); 318 | assertEquals(USER_NOT_FOUND, error.getCode()); 319 | 320 | } 321 | } 322 | -------------------------------------------------------------------------------- /src/test/java/com/authy/api/TokensTest.java: -------------------------------------------------------------------------------- 1 | package com.authy.api; 2 | 3 | import com.authy.AuthyApiClient; 4 | import com.authy.AuthyException; 5 | import org.junit.Assert; 6 | import org.junit.Before; 7 | import org.junit.Test; 8 | import sun.net.www.protocol.http.HttpURLConnection; 9 | 10 | import java.util.HashMap; 11 | import java.util.Map; 12 | 13 | import static com.authy.api.Error.Code.TOKEN_INVALID; 14 | import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; 15 | import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; 16 | import static com.github.tomakehurst.wiremock.client.WireMock.get; 17 | import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; 18 | import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; 19 | import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; 20 | import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching; 21 | import static com.github.tomakehurst.wiremock.client.WireMock.verify; 22 | import static junit.framework.TestCase.assertEquals; 23 | import static junit.framework.TestCase.fail; 24 | 25 | public class TokensTest extends TestApiBase { 26 | 27 | private Tokens tokens; 28 | private int testUserId = 123456; 29 | private String testToken = "123456"; 30 | 31 | private final String invalidTokenResponse = "{" 32 | + " \"message\": \"Token is invalid\"," 33 | + " \"token\": \"is invalid\"," 34 | + " \"success\": false," 35 | + " \"errors\": {" 36 | + " \"message\": \"Token is invalid\"" 37 | + " },\n" 38 | + " \"error_code\": \"60020\"" 39 | + "}"; 40 | 41 | private final String validTokenResponse = "{" 42 | + " \"message\": \"Token is valid.\"," 43 | + " \"token\": \"is valid\"," 44 | + " \"success\": \"true\"," 45 | + " \"device\": {" 46 | + " \"id\": null," 47 | + " \"os_type\": \"sms\"," 48 | + " \"registration_date\": 1500648405," 49 | + " \"registration_method\": null," 50 | + " \"registration_country\": null," 51 | + " \"registration_region\": null," 52 | + " \"registration_city\": null," 53 | + " \"country\": null," 54 | + " \"region\": null," 55 | + " \"city\": null," 56 | + " \"ip\": null," 57 | + " \"last_account_recovery_at\": 1494631010," 58 | + " \"last_sync_date\": null" 59 | + " }" 60 | + "}"; 61 | 62 | private final String invalidErrorFormatResponse = "" 63 | + " \"message\": \"Token is invalid\"," 64 | + " \"token\": \"is invalid\"," 65 | + " \"success\": false," 66 | + " \"errors\": {" 67 | + " \"message\": \"Token is invalid\"" 68 | + " }," 69 | + " \"error_code\": \"60019\"" 70 | + "}"; 71 | 72 | @Before 73 | public void setUp() { 74 | tokens = new AuthyApiClient(testApiKey, testHost, true).getTokens(); 75 | } 76 | 77 | @Test 78 | public void tokenFormatValidation() { 79 | String alphaToken = "abcde"; 80 | try { 81 | tokens.verify(0, alphaToken); 82 | fail("Tokens must be numeric"); 83 | } catch (Exception e) { 84 | Assert.assertTrue("Proper exception must be thrown", e instanceof AuthyException); 85 | } 86 | } 87 | 88 | @Test 89 | public void tokenLengthValidation() { 90 | String shortToken = "123"; 91 | try { 92 | tokens.verify(0, shortToken); 93 | fail("Tokens must be between 6 and 10 digits"); 94 | } catch (Exception e) { 95 | Assert.assertTrue("Proper exception must be thrown", e instanceof AuthyException); 96 | } 97 | 98 | String longToken = "12345678901"; 99 | try { 100 | tokens.verify(0, longToken); 101 | fail("Tokens must be between 6 and 10 digits"); 102 | } catch (Exception e) { 103 | Assert.assertTrue("Proper exception must be thrown", e instanceof AuthyException); 104 | } 105 | } 106 | 107 | @Test 108 | public void testInvalidTokenResponse() { 109 | stubFor(get(urlPathMatching("/protected/json/verify/.*")) 110 | .willReturn(aResponse() 111 | .withStatus(HttpURLConnection.HTTP_UNAUTHORIZED) 112 | .withHeader("Content-Type", "application/json") 113 | .withBody(invalidTokenResponse))); 114 | 115 | 116 | try { 117 | tokens.verify(testUserId, testToken); 118 | fail("Exception should have been thrown"); 119 | } catch (AuthyException e) { 120 | assertEquals(TOKEN_INVALID, e.getErrorCode()); 121 | assertEquals(HttpURLConnection.HTTP_UNAUTHORIZED, e.getStatus()); 122 | } 123 | } 124 | 125 | @Test 126 | public void testInvalidTokenLength() { 127 | try { 128 | tokens.verify(testUserId, "12345678901234567890"); 129 | fail("Exception should have been thrown"); 130 | } catch (AuthyException e) { 131 | assertEquals(TOKEN_INVALID, e.getErrorCode()); 132 | assertEquals(HttpURLConnection.HTTP_BAD_REQUEST, e.getStatus()); 133 | } 134 | } 135 | 136 | @Test 137 | public void testInvalidTokenCharacters() { 138 | stubFor(get(urlPathMatching("/protected/json/verify/.*")) 139 | .willReturn(aResponse() 140 | .withStatus(401) 141 | .withHeader("Content-Type", "application/json") 142 | .withBody(invalidTokenResponse))); 143 | 144 | 145 | try { 146 | tokens.verify(testUserId, "asdasdadasdasdasdasda"); 147 | fail("Exception should have been thrown"); 148 | } catch (AuthyException e) { 149 | assertEquals(TOKEN_INVALID, e.getErrorCode()); 150 | } 151 | } 152 | 153 | @Test 154 | public void testValidTokenResponse() { 155 | stubFor(get(urlPathMatching("/protected/json/verify/.*")) 156 | .willReturn(aResponse() 157 | .withStatus(200) 158 | .withHeader("Content-Type", "application/json") 159 | .withBody(validTokenResponse))); 160 | 161 | 162 | try { 163 | Token token = tokens.verify(testUserId, testToken); 164 | Assert.assertNull("Token must not have an error", token.getError()); 165 | Assert.assertTrue("Token verification must be successful", token.isOk()); 166 | } catch (AuthyException e) { 167 | fail("Verification should be successful"); 168 | } 169 | } 170 | 171 | @Test 172 | public void testVerificationOptions() { 173 | stubFor(get(urlPathMatching("/protected/json/verify/.*")) 174 | .willReturn(aResponse() 175 | .withStatus(200) 176 | .withHeader("Content-Type", "application/json") 177 | .withBody(validTokenResponse))); 178 | 179 | 180 | try { 181 | Map options = new HashMap<>(); 182 | options.put("force", "false"); 183 | Token token = tokens.verify(testUserId, testToken, options); 184 | Assert.assertNull("Token must not have an error", token.getError()); 185 | Assert.assertTrue("Token verification must be successful", token.isOk()); 186 | } catch (AuthyException e) { 187 | fail("Verification should be successful"); 188 | } 189 | } 190 | 191 | @Test 192 | public void testInvalidErrorFormatResponse() { 193 | stubFor(get(urlPathMatching("/protected/json/verify/.*")) 194 | .willReturn(aResponse() 195 | .withStatus(401) 196 | .withHeader("Content-Type", "application/json") 197 | .withBody(invalidErrorFormatResponse))); 198 | 199 | 200 | try { 201 | tokens.verify(testUserId, testToken); 202 | fail("Exception must be thrown"); 203 | } catch (AuthyException e) { 204 | return; 205 | } 206 | fail("Proper exception must be thrown"); 207 | } 208 | 209 | @Test 210 | public void testRequestParameters() { 211 | stubFor(get(urlPathMatching("/protected/json/verify/.*")) 212 | .willReturn(aResponse() 213 | .withStatus(200) 214 | .withHeader("Content-Type", "application/json") 215 | .withBody(validTokenResponse))); 216 | 217 | 218 | try { 219 | Token token = tokens.verify(testUserId, testToken); 220 | 221 | verify(getRequestedFor(urlPathEqualTo("/protected/json/verify/" + testToken + "/" + testUserId)) 222 | .withHeader("X-Authy-API-Key", equalTo(testApiKey))); 223 | Assert.assertNull("Token must not have an error", token.getError()); 224 | Assert.assertTrue("Token verification must be successful", token.isOk()); 225 | } catch (AuthyException e) { 226 | fail("Verification should be successful"); 227 | } 228 | } 229 | } -------------------------------------------------------------------------------- /src/test/java/com/authy/api/UserStatusTest.java: -------------------------------------------------------------------------------- 1 | package com.authy.api; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | import java.util.Map; 9 | 10 | import static org.junit.Assert.assertEquals; 11 | import static org.junit.Assert.assertNotNull; 12 | 13 | public class UserStatusTest { 14 | 15 | public static final int USER_ID = 1234; 16 | public static final String PHONE_NUMBER = "456 758 8990"; 17 | public static final String DEVICE_A = "deviceA"; 18 | public static final String DEVICE_B = "deviceB"; 19 | private UserStatus userStatus; 20 | 21 | @Before 22 | public void setup() { 23 | userStatus = new UserStatus(); 24 | userStatus.setStatus(200); 25 | userStatus.setUserId(USER_ID); 26 | userStatus.setSuccess(true); 27 | userStatus.setConfirmed(true); 28 | userStatus.setRegistered(true); 29 | userStatus.setCountryCode(1); 30 | userStatus.setPhoneNumber(PHONE_NUMBER); 31 | List devices = new ArrayList<>(); 32 | devices.add(DEVICE_A); 33 | devices.add(DEVICE_B); 34 | userStatus.setDevices(devices); 35 | } 36 | 37 | @Test 38 | public void testToMap() { 39 | Map userStatusMap = userStatus.toMap(); 40 | assertNotNull(userStatusMap); 41 | assertEquals(Integer.toString(USER_ID), userStatusMap.get("userId")); 42 | assertEquals(Boolean.toString(true), userStatusMap.get("success")); 43 | assertEquals(Boolean.toString(true), userStatusMap.get("confirmed")); 44 | assertEquals(Boolean.toString(true), userStatusMap.get("registered")); 45 | assertEquals(Integer.toString(1), userStatusMap.get("countryCode")); 46 | assertEquals(PHONE_NUMBER, userStatusMap.get("phoneNumber")); 47 | assertEquals(String.format("[%s, %s]", DEVICE_A, DEVICE_B), userStatusMap.get("devices")); 48 | } 49 | 50 | @Test 51 | public void testToXML() { 52 | String userStatusXml = userStatus.toXML(); 53 | assertNotNull(userStatusXml); 54 | assertEquals("" + 55 | "2001234truetrue" + 56 | "true1" + 57 | "deviceAdeviceB" + 58 | "456 758 8990", 59 | userStatusXml); 60 | } 61 | 62 | @Test 63 | public void testToJSON() { 64 | String userStatusJson = userStatus.toJSON(); 65 | assertNotNull(userStatusJson); 66 | assertEquals(userStatusJson, "{\"phoneNumber\":\"456 758 8990\"," + 67 | "\"devices\":\"[deviceA, deviceB]\",\"success\":\"true\"," + 68 | "\"countryCode\":\"1\",\"registered\":\"true\",\"userId\":\"1234\",\"confirmed\":\"true\"}"); 69 | } 70 | } -------------------------------------------------------------------------------- /verify-legacy-v1.md: -------------------------------------------------------------------------------- 1 | # Phone Verification V1 2 | 3 | [Version 2 of the Verify API is now available!](https://www.twilio.com/docs/verify/api) V2 has an improved developer experience and new features. Some of the features of the V2 API include: 4 | 5 | * Twilio helper libraries in JavaScript, Java, C#, Python, Ruby, and PHP 6 | * PSD2 Secure Customer Authentication Support 7 | * Improved Visibility and Insights 8 | 9 | **You are currently viewing Version 1. V1 of the API will be maintained for the time being, but any new features and development will be on Version 2. We strongly encourage you to do any new development with API V2.** Check out the [migration guide](https://www.twilio.com/docs/verify/api/migrating-1x-2x) or the API Reference for more information. 10 | 11 | ### API Reference 12 | 13 | API Reference is available at https://www.twilio.com/docs/verify/api/v1 14 | 15 | ### Sending the verification code. 16 | 17 | ```java 18 | AuthyApiClient client = new AuthyApiClient("SomeApiKey"); 19 | PhoneVerification phoneVerification = client.getPhoneVerification(); 20 | 21 | Verification verification; 22 | Params params = new Params(); 23 | params.setAttribute("locale", en); 24 | 25 | verification = phoneVerification.start("111-111-1111", "1", "sms", params); 26 | 27 | System.out.println(verification.getMessage()); 28 | System.out.println(verification.getIsPorted()); 29 | System.out.println(verification.getSuccess()); 30 | System.out.println(verification.isOk()); 31 | ``` 32 | 33 | ### Check the verification code. 34 | Once you sent the verification code the user will receive the code in the 35 | mobile device. Then you need to provide this code to check if it is okay. 36 | 37 | 38 | ```java 39 | AuthyApiClient client = new AuthyApiClient("SomeApiKey"); 40 | PhoneVerification phoneVerification = client.getPhoneVerification(); 41 | 42 | Verification verification; 43 | verification = phoneVerification.check("111-111-1111", "1", "2061"); 44 | 45 | System.out.println(verificationCode.getMessage()); 46 | System.out.println(verificationCode.getIsPorted()); 47 | System.out.println(verificationCode.getSuccess()); 48 | ``` --------------------------------------------------------------------------------