├── .gitignore ├── README.md ├── build.gradle ├── gradle └── wrapper │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src ├── main └── kotlin │ └── org │ └── jetbrains │ └── hub │ └── oauth2 │ └── client │ ├── AccessToken.kt │ ├── AccessTokenSource.kt │ ├── AuthException.kt │ ├── AuthHeader.kt │ ├── Base64.kt │ ├── BasicAuth.kt │ ├── OAuth2Client.kt │ ├── jersey │ └── JerseyClientTokenLoader.kt │ └── loader │ ├── AuthRequest.kt │ ├── TokenLoader.kt │ └── TokenResponse.kt └── test └── kotlin └── org └── jetbrains └── hub └── oauth2 └── client ├── AccessTokenSourceSpekTrait.kt ├── AccessTokenSpekTrait.kt ├── AuthURISpekTrait.kt ├── Base64Spek.kt ├── ClientFlowSpek.kt ├── CodeFlowSpek.kt ├── DateUtil.kt ├── ImplicitFlowSpek.kt ├── MockTokenLoader.kt ├── RefreshTokenFlowSpek.kt ├── ResourceOwnerFlowSpek.kt └── sample └── Hub.kt /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Gradle template 3 | .gradle 4 | build/ 5 | 6 | # Ignore Gradle GUI config 7 | gradle-app.setting 8 | 9 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 10 | !gradle-wrapper.jar 11 | ### JetBrains template 12 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio 13 | 14 | *.iml 15 | 16 | ## Directory-based project format: 17 | .idea/ 18 | # if you remove the above rule, at least ignore the following: 19 | 20 | # User-specific stuff: 21 | # .idea/workspace.xml 22 | # .idea/tasks.xml 23 | # .idea/dictionaries 24 | 25 | # Sensitive or high-churn files: 26 | # .idea/dataSources.ids 27 | # .idea/dataSources.xml 28 | # .idea/sqlDataSources.xml 29 | # .idea/dynamic.xml 30 | # .idea/uiDesigner.xml 31 | 32 | # Gradle: 33 | # .idea/gradle.xml 34 | # .idea/libraries 35 | 36 | # Mongo Explorer plugin: 37 | # .idea/mongoSettings.xml 38 | 39 | ## File-based project format: 40 | *.ipr 41 | *.iws 42 | 43 | ## Plugin-specific files: 44 | 45 | # IntelliJ 46 | /out/ 47 | 48 | # mpeltonen/sbt-idea plugin 49 | .idea_modules/ 50 | 51 | # JIRA plugin 52 | atlassian-ide-plugin.xml 53 | 54 | # Crashlytics plugin (for Android Studio and IntelliJ) 55 | com_crashlytics_export_strings.xml 56 | crashlytics.properties 57 | crashlytics-build.properties 58 | ### Java template 59 | *.class 60 | 61 | # Mobile Tools for Java (J2ME) 62 | .mtj.tmp/ 63 | 64 | # Package Files # 65 | *.jar 66 | *.war 67 | *.ear 68 | 69 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 70 | hs_err_pid* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | Tiny OAuth 2.0 client library in Kotlin that supports [JetBrains Hub](http://jetbrains.com/hub) authorization 3 | features. 4 | 5 | In the most cases to enable authorization via OAuth 2.0 server you have to know 6 | `Client ID` and `Client Secret` that you get when you register your application at the 7 | OAuth 2.0 server. During the registration you often provide a `Redirect URI` to the OAuth 2.0 server. 8 | This URI is actually a URI in your application that handles responses from the OAuth 2.0 server. 9 | 10 | To perform operations authorized by the OAuth 2.0 server, your application requires an `Access Token`. There 11 | are several ways (so called flows) for your applications to get it. The flow you will use depends on the 12 | environment your application runs in. 13 | 14 | ### Gradle and Maven 15 | [![Release](https://jitpack.io/v/mazine/oauth2-client-kotlin.svg)](https://jitpack.io/#mazine/oauth2-client-kotlin) 16 | 17 | #### Gradle 18 | ``` groovy 19 | repositories { 20 | jcenter() 21 | maven { url "https://jitpack.io" } 22 | } 23 | dependencies { 24 | compile 'com.github.mazine:oauth2-client-kotlin:$version' 25 | } 26 | ``` 27 | 28 | #### Maven 29 | ``` xml 30 | 31 | 32 | jitpack.io 33 | https://jitpack.io 34 | 35 | 36 | ``` 37 | ``` 38 | 39 | com.github.mazine 40 | oauth2-client-kotlin 41 | $version 42 | 43 | ``` 44 | 45 | ## Usage 46 | 47 | ### Code Flow 48 | 49 | **Use it if** 50 | - Your application is running on a web server. 51 | - Your application builds HTML responses on the server side. 52 | - The `Client ID`, `Client Secret` and any access token issued to your application are stored on the web server 53 | and are not accessible by the end-user. 54 | 55 | **How It Works?** 56 | 1. User tries to access your application via browser and your application finds out that it doesn't have yet an `Access 57 | Token` for the user. 58 | 2. Your application saves the information about the URI, user tried to access, under the unique identifier `state`. 59 | 3. Your application redirects the user to the OAuth 2.0 server passing the `state` as one of the parameters. 60 | 4. User identifies herself at the OAuth 2.0 server (e.g. by entering her username and password). 61 | 5. OAuth 2.0 server redirects the user back to your application (to the `Redirect URI` to be precise) with two query 62 | parameters: `state` and `code`. 63 | 6. Your application makes a server to server HTTP call to the OAuth 2.0 server exchanging the `code` for the `Access 64 | Token`. To identify itself your application passes with the call its `Client ID` and `Client Secret`. 65 | 7. Your application using the `state` restores the URI originally requested by the user, and redirects her there. 66 | 67 | For further details check [OAuth 2.0 Spec](https://tools.ietf.org/html/rfc6749#section-4.1) 68 | or [Hub Docs](https://www.jetbrains.com/help/hub/2.0/Authorization-Code.html). 69 | 70 | The library actually helps to build the URI to redirect the user on the step 3, and to exchange the `code` for 71 | the `Access Token` on the step 6. 72 | 73 | **Build URI** 74 | ``` 75 | val targetURI = oauth2Client().codeFlowURI( 76 | authEndpoint = URI("https://hub.jetbrains.com/api/rest/oauth2/auth"), 77 | clientID = "1234-3213-3123", 78 | redirectURI = URI("https://localhost:8080/auth"), 79 | scope = listOf("0-0-0-0-0", clientID), 80 | state = "some-unique-state-id") 81 | ``` 82 | 83 | **Exchange code** 84 | ``` 85 | val accessToken = oauth2Client().codeFlow( 86 | tokenEndpoint = URI("https://hub.jetbrains.com/api/rest/oauth2/token"), 87 | code = "sOMec0de", 88 | redirectURI = URI("https://localhost:8080/auth"), 89 | clientID = "1234-3213-3123", 90 | clientSecret = "sGUl4x") 91 | 92 | do { 93 | // Make various calls using accessToken.header 94 | } while (!accessToken.isExpired) 95 | ``` 96 | 97 | ### Client Flow 98 | 99 | **Use it if** 100 | - Your application accesses resources on behalf of itself. 101 | - The `Client ID`, `Client Secret` and any access token issued to your application are stored confident. 102 | 103 | For further details check [OAuth 2.0 Spec](http://tools.ietf.org/html/rfc6749#section-4.4) 104 | or [Hub Docs](https://www.jetbrains.com/help/hub/2.0/Client-Credentials.html). 105 | 106 | The library allows to create an `AccessTokenSource` for this flow. It is an object that retrieves and 107 | caches an `Access Token`, and renews the `Access Token` when it expires. 108 | 109 | ``` 110 | val tokenSource = oauth2Client().clientFlow( 111 | tokenEndpoint = URI("https://hub.jetbrains.com/api/rest/oauth2/token"), 112 | clientID = "1234-3213-3123", 113 | clientSecret = "sGUl4x", 114 | scope = listOf("0-0-0-0-0", clientID)) 115 | 116 | do { 117 | // Make various calls using tokenSource.accessToken.header 118 | } while (true) 119 | ``` 120 | 121 | ### Resource Owner Flow 122 | 123 | **Use it if** 124 | 125 | Your application knows user credentials and accesses resources on behalf of a user. For example, your application is 126 | the device operating system or a highly privileged application. 127 | 128 | For further details check [OAuth 2.0 Spec](http://tools.ietf.org/html/rfc6749#section-4.3) 129 | or [Hub Docs](https://www.jetbrains.com/help/hub/2.0/Resource-Owner-Password-Credentials.html). 130 | 131 | The library allows to create an `AccessTokenSource` for this flow. 132 | 133 | ``` 134 | val tokenSource = oauth2Client().resourceOwnerFlow( 135 | tokenEndpoint = URI("https://hub.jetbrains.com/api/rest/oauth2/token"), 136 | username = "john.doe", 137 | password = "p@$Sw0rd", 138 | clientID = "1234-3213-3123", 139 | clientSecret = "sGUl4x", 140 | scope = listOf("0-0-0-0-0", clientID)) 141 | 142 | do { 143 | // Make various calls using tokenSource.accessToken.header 144 | } while (true) 145 | ``` 146 | 147 | ### Implicit Flow 148 | 149 | **Use it if** 150 | 151 | Your application is public. Typically a JavaScript code in a browser. 152 | 153 | **How It Works?** 154 | 1. User downloads your JavaScript application into her browser. To access resources via REST API your application 155 | needs an `Access Token`. 156 | 2. Your application finds out that it has no `Access Token` yet. 157 | 3. Your application saves the information about the URI, user tried to access, under the unique identifier `state` 158 | (e.g. in a local storage of the browser). 159 | 4. Your application redirects the user to the OAuth 2.0 server passing the `state` as one of the parameters. 160 | 5. User identifies herself at the OAuth 2.0 server (e.g. by entering her username and password). 161 | 6. OAuth 2.0 server redirects the user back to your application (to the `Redirect URI` to be precise) with an 162 | `Access Token` in parameters after ‘`#`’. The trick here is that browser sends nothing after ‘`#`’ in URL to 163 | the server, but the part after ‘`#`’ is accessible for your JavaScript application. So the `Access Token` never 164 | leaves user's browser. 165 | 166 | For further details check [OAuth 2.0 Spec](http://tools.ietf.org/html/rfc6749#section-4.2) 167 | or [Hub Docs](https://www.jetbrains.com/help/hub/2.0/Implicit.html). 168 | 169 | The library only helps to build the URI to redirect the user on the step 4. 170 | 171 | ``` 172 | val targetURI = oauth2Client().implicitFlowURI( 173 | authEndpoint = URI("https://hub.jetbrains.com/api/rest/oauth2/auth"), 174 | clientID = "1234-3213-3123", 175 | redirectURI = URI("https://localhost:8080/auth"), 176 | scope = listOf("0-0-0-0-0", clientID), 177 | state = "some-unique-state-id") 178 | ``` 179 | 180 | ### Refresh Token 181 | 182 | **Use it if** 183 | 184 | Your application is a desktop or mobile application that wants to access resources on behalf of user, 185 | when the user is offline. 186 | 187 | For further details check [OAuth 2.0 Spec](https://tools.ietf.org/html/rfc6749#section-4.1) 188 | or [Hub Docs](https://www.jetbrains.com/help/hub/2.0/Refresh-Token.html). 189 | 190 | Your application can obtain a `Refresh Token` as a part of [code](#code-flow) or [resource owner](#resource-owner-flow) 191 | flows. 192 | 193 | **Obtain `Refresh Token` from code flow** 194 | 195 | When redirect to OAuth 2.0 server, request refreshToken 196 | ``` 197 | oauth2Client().codeFlowURI( 198 | authEndpoint = URI("https://hub.jetbrains.com/api/rest/oauth2/auth"), 199 | clientID = "1234-3213-3123", 200 | redirectURI = URI("https://localhost:8080/auth"), 201 | scope = listOf("0-0-0-0-0", clientID), 202 | state = "some-unique-state-id", 203 | requestRefreshToken = true) 204 | ``` 205 | 206 | When user returns with a `code`, use the `code` to obtain refresh token 207 | ``` 208 | val refreshToken = oauth2Client().codeRefreshToken( 209 | tokenEndpoint = URI("https://hub.jetbrains.com/api/rest/oauth2/token"), 210 | code = "sOMec0de", 211 | redirectURI = URI("https://localhost:8080/auth"), 212 | clientID = "1234-3213-3123", 213 | clientSecret = "sGUl4x") 214 | ``` 215 | 216 | **Obtain `Refresh Token` from resource owner flow** 217 | ``` 218 | val refreshToken = oauth2Client().resourceOwnerRefreshToken( 219 | tokenEndpoint = URI("https://hub.jetbrains.com/api/rest/oauth2/token"), 220 | username = "john.doe", 221 | password = "p@$Sw0rd", 222 | clientID = "1234-3213-3123", 223 | clientSecret = "sGUl4x", 224 | scope = listOf("0-0-0-0-0", clientID)) 225 | ``` 226 | 227 | **Use `Refresh Token` to get `AccessTokenSource`** 228 | ``` 229 | val tokenSource = oauth2Client().refreshTokenFlow( 230 | tokenEndpoint = URI("https://hub.jetbrains.com/api/rest/oauth2/token"), 231 | refreshToken = "that-refresh-token", 232 | clientID = "1234-3213-3123", 233 | clientSecret = "sGUl4x", 234 | scope = listOf("0-0-0-0-0", clientID)) 235 | 236 | do { 237 | // Make various calls using tokenSource.accessToken.header 238 | } while (true) 239 | ``` 240 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | group 'com.github.mazine' 2 | version '1.0.1' 3 | 4 | buildscript { 5 | ext.kotlin_version = '1.0.2' 6 | repositories { 7 | mavenCentral() 8 | } 9 | dependencies { 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | apply plugin: 'java' 15 | apply plugin: 'kotlin' 16 | apply plugin: 'idea' 17 | apply plugin: 'maven' 18 | apply plugin: 'maven-publish' 19 | 20 | idea { 21 | project { 22 | jdkName = '1.7' 23 | vcs = 'Git' 24 | } 25 | } 26 | 27 | sourceCompatibility = 1.7 28 | 29 | repositories { 30 | mavenCentral() 31 | maven { url "http://repository.jetbrains.com/all" } 32 | } 33 | 34 | dependencies { 35 | compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 36 | compile 'org.glassfish.jersey.core:jersey-common:2.21' 37 | compile 'org.glassfish.jersey.core:jersey-client:2.21' 38 | compile 'org.slf4j:slf4j-api:1.7.12' 39 | compile 'org.slf4j:slf4j-log4j12:1.7.12' 40 | 41 | testCompile group: 'junit', name: 'junit', version: '4.11' 42 | testCompile 'org.jetbrains.spek:spek:1.0.25' 43 | } 44 | 45 | 46 | task sourcesJar(type: Jar, dependsOn: classes) { 47 | classifier = 'sources' 48 | from sourceSets.main.allSource 49 | } 50 | 51 | artifacts { 52 | archives sourcesJar 53 | } 54 | 55 | install { 56 | repositories.mavenInstaller { 57 | pom.project { 58 | licenses { 59 | license { 60 | name 'The Apache Software License, Version 2.0' 61 | url 'http://www.apache.org/licenses/LICENSE-2.0.txt' 62 | distribution 'repo' 63 | } 64 | } 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Jun 15 17:36:28 NOVT 2016 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.9-bin.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'oauth2-client-kotlin' 2 | 3 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jetbrains/hub/oauth2/client/AccessToken.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.hub.oauth2.client 2 | 3 | import java.util.* 4 | 5 | 6 | open class AccessToken( 7 | val accessToken: String, 8 | val expiresAt: Calendar, 9 | val scope: List) { 10 | 11 | val isExpired: Boolean 12 | get() = Calendar.getInstance().after(expiresAt) 13 | 14 | val header: AuthHeader 15 | get() = AuthHeader("Bearer $accessToken") 16 | } 17 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jetbrains/hub/oauth2/client/AccessTokenSource.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.hub.oauth2.client 2 | 3 | import java.util.concurrent.atomic.AtomicReference 4 | 5 | abstract class AccessTokenSource() { 6 | private val cachedToken = AtomicReference() 7 | 8 | val accessToken: AccessToken 9 | get() { 10 | val current: AccessToken? = cachedToken.get() 11 | val newToken = when { 12 | current == null -> loadToken() 13 | current.isExpired -> loadToken() 14 | else -> current 15 | } 16 | cachedToken.compareAndSet(current, newToken) 17 | return newToken 18 | } 19 | 20 | protected abstract fun loadToken(): AccessToken 21 | } 22 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jetbrains/hub/oauth2/client/AuthException.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.hub.oauth2.client 2 | 3 | class AuthException(val error: String, val errorDescription: String?) : 4 | RuntimeException(errorDescription ?: "Authentication error: $error") -------------------------------------------------------------------------------- /src/main/kotlin/org/jetbrains/hub/oauth2/client/AuthHeader.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.hub.oauth2.client 2 | 3 | /** 4 | * Data class for authorization header name and value 5 | */ 6 | data class AuthHeader(val value: String) { 7 | val name: String 8 | get() = "Authorization" 9 | } -------------------------------------------------------------------------------- /src/main/kotlin/org/jetbrains/hub/oauth2/client/Base64.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.hub.oauth2.client 2 | 3 | object Base64 { 4 | private val PEM_ARRAY = sequenceOf( 5 | ('A'..'Z').asSequence(), 6 | ('a'..'z').asSequence(), 7 | ('0'..'9').asSequence(), 8 | sequenceOf('+', '/') 9 | ).flatMap { it }.toList().toCharArray() 10 | 11 | private val FILL = '=' 12 | 13 | 14 | fun encode(bytes: ByteArray): String { 15 | val builder = StringBuilder((bytes.size * 4 + 1) / 3) 16 | if (bytes.isNotEmpty()) { 17 | for (i in (0..bytes.lastIndex / 3)) { 18 | val base = i * 3 19 | val b1 = bytes.get(base).toInt() 20 | val b2 = bytes.getOrNull(base + 1)?.toInt() 21 | val b3 = bytes.getOrNull(base + 2)?.toInt() 22 | 23 | builder.append(PEM_ARRAY[(b1 ushr 2) and 63]) 24 | builder.append(PEM_ARRAY[((b1 shl 4) and 48) + ((b2 ?: 0) ushr 4 and 15)]) 25 | if (b2 != null) { 26 | builder.append(PEM_ARRAY[((b2 shl 2) and 60) + ((b3 ?: 0) ushr 6 and 3)]) 27 | if (b3 != null) { 28 | builder.append(PEM_ARRAY[b3 and 63]) 29 | } else { 30 | builder.append(FILL) 31 | } 32 | } else { 33 | builder.append(FILL) 34 | builder.append(FILL) 35 | } 36 | 37 | } 38 | } 39 | return builder.toString() 40 | } 41 | 42 | 43 | } -------------------------------------------------------------------------------- /src/main/kotlin/org/jetbrains/hub/oauth2/client/BasicAuth.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.hub.oauth2.client 2 | 3 | class BasicAuth(val username: String, val password: String) { 4 | val header: AuthHeader 5 | get() = AuthHeader("Basic ${Base64.encode("$username:$password".toByteArray())}") 6 | } -------------------------------------------------------------------------------- /src/main/kotlin/org/jetbrains/hub/oauth2/client/OAuth2Client.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.hub.oauth2.client 2 | 3 | import org.jetbrains.hub.oauth2.client.loader.* 4 | import java.net.URI 5 | 6 | class OAuth2Client(val tokenLoader: TokenLoader) { 7 | fun clientFlow( 8 | tokenEndpoint: URI, 9 | clientID: String, 10 | clientSecret: String, 11 | scope: List, 12 | authTransport: ClientAuthTransport = ClientAuthTransport.HEADER): AccessTokenSource { 13 | return refreshableTokenSource(TokenRequest(tokenEndpoint, clientID, clientSecret).apply { 14 | this.grantType = GrantType.CLIENT_CREDENTIALS 15 | this.scope = scope 16 | this.authTransport = authTransport 17 | }) 18 | } 19 | 20 | fun resourceOwnerFlow( 21 | tokenEndpoint: URI, 22 | username: String, password: String, 23 | clientID: String, clientSecret: String, 24 | scope: List, 25 | authTransport: ClientAuthTransport = ClientAuthTransport.HEADER): AccessTokenSource { 26 | return refreshableTokenSource(resourceOwnerTokenRequest( 27 | tokenEndpoint, username, password, 28 | clientID, clientSecret, 29 | scope, authTransport, requestRefreshToken = false)) 30 | } 31 | 32 | fun resourceOwnerRefreshToken( 33 | tokenEndpoint: URI, 34 | username: String, password: String, 35 | clientID: String, clientSecret: String, 36 | scope: List, 37 | authTransport: ClientAuthTransport = ClientAuthTransport.HEADER): String { 38 | return tokenLoader. 39 | load(resourceOwnerTokenRequest( 40 | tokenEndpoint, username, password, 41 | clientID, clientSecret, 42 | scope, authTransport, requestRefreshToken = true)). 43 | asRefreshToken() 44 | } 45 | 46 | private fun resourceOwnerTokenRequest( 47 | tokenEndpoint: URI, 48 | username: String, password: String, 49 | clientID: String, clientSecret: String, 50 | scope: List, 51 | authTransport: ClientAuthTransport = ClientAuthTransport.HEADER, 52 | requestRefreshToken: Boolean = false): TokenRequest { 53 | return TokenRequest(tokenEndpoint, clientID, clientSecret).apply { 54 | this.grantType = GrantType.PASSWORD 55 | this.scope = scope 56 | this.authTransport = authTransport 57 | this.username = username 58 | this.password = password 59 | this.requestRefreshToken = requestRefreshToken 60 | } 61 | } 62 | 63 | 64 | fun codeFlowURI( 65 | authEndpoint: URI, 66 | clientID: String, 67 | redirectURI: URI, 68 | scope: List, 69 | state: String, 70 | requestRefreshToken: Boolean = false, 71 | message: String? = null, 72 | prompt: PromptApproval? = null, 73 | requestCredentials: RequestCredentials = RequestCredentials.DEFAULT): URI { 74 | 75 | return tokenLoader.authURI(AuthRequest(authEndpoint, clientID).apply { 76 | this.authResponseType = ResponseType.CODE 77 | this.state = state 78 | this.redirectURI = redirectURI 79 | this.scope = scope 80 | this.message = message 81 | this.requestRefreshToken = requestRefreshToken 82 | this.prompt = prompt 83 | this.requestCredentials = requestCredentials 84 | }) 85 | } 86 | 87 | fun codeFlow( 88 | tokenEndpoint: URI, 89 | code: String, 90 | redirectURI: URI, 91 | clientID: String, 92 | clientSecret: String, 93 | authTransport: ClientAuthTransport = ClientAuthTransport.HEADER): AccessToken { 94 | val response = tokenLoader.load(codeFlowTokenRequest( 95 | tokenEndpoint, code, redirectURI, 96 | clientID, clientSecret, authTransport)) 97 | 98 | return response.asAccessToken() 99 | } 100 | 101 | fun codeRefreshToken( 102 | tokenEndpoint: URI, 103 | code: String, 104 | redirectURI: URI, 105 | clientID: String, 106 | clientSecret: String, 107 | authTransport: ClientAuthTransport = ClientAuthTransport.HEADER): String { 108 | return tokenLoader. 109 | load(codeFlowTokenRequest( 110 | tokenEndpoint, code, redirectURI, 111 | clientID, clientSecret, authTransport)). 112 | asRefreshToken() 113 | } 114 | 115 | private fun codeFlowTokenRequest( 116 | tokenEndpoint: URI, 117 | code: String, 118 | redirectURI: URI, 119 | clientID: String, 120 | clientSecret: String, 121 | authTransport: ClientAuthTransport = ClientAuthTransport.HEADER): TokenRequest { 122 | return TokenRequest(tokenEndpoint, clientID, clientSecret).apply { 123 | this.grantType = GrantType.AUTHORIZATION_CODE 124 | this.authTransport = authTransport 125 | this.redirectURI = redirectURI 126 | this.code = code 127 | } 128 | } 129 | 130 | fun implicitFlowURI( 131 | authEndpoint: URI, 132 | clientID: String, 133 | redirectURI: URI, 134 | scope: List, 135 | state: String, 136 | message: String? = null, 137 | prompt: PromptApproval? = null, 138 | requestCredentials: RequestCredentials = RequestCredentials.DEFAULT): URI { 139 | 140 | return tokenLoader.authURI(AuthRequest(authEndpoint, clientID).apply { 141 | this.authResponseType = ResponseType.TOKEN 142 | this.state = state 143 | this.redirectURI = redirectURI 144 | this.scope = scope 145 | this.message = message 146 | this.prompt = prompt 147 | this.requestCredentials = requestCredentials 148 | }) 149 | } 150 | 151 | fun refreshTokenFlow(tokenEndpoint: URI, 152 | refreshToken: String, 153 | clientID: String, 154 | clientSecret: String, 155 | scope: List, 156 | authTransport: ClientAuthTransport = ClientAuthTransport.HEADER): AccessTokenSource { 157 | return refreshableTokenSource(TokenRequest(tokenEndpoint, clientID, clientSecret).apply { 158 | this.grantType = GrantType.REFRESH_TOKEN 159 | this.refreshToken = refreshToken 160 | this.scope = scope 161 | this.authTransport = authTransport 162 | }) 163 | } 164 | 165 | private fun refreshableTokenSource(tokenRequest: TokenRequest): AccessTokenSource { 166 | return object : AccessTokenSource() { 167 | override fun loadToken(): AccessToken { 168 | val response = tokenLoader.load(tokenRequest) 169 | return response.asAccessToken() 170 | } 171 | } 172 | } 173 | 174 | private fun TokenLoader.authURI(authRequest: AuthRequest): URI { 175 | return authURI(authRequest.uri, authRequest.queryParameters.mapNotNull()) 176 | } 177 | 178 | private fun Sequence>.mapNotNull(): Map { 179 | return mapNotNull { 180 | it.second?.let { value -> it.first to value } 181 | }.toMap() 182 | } 183 | 184 | private fun TokenLoader.load(tokenRequest: TokenRequest): TokenResponse { 185 | return load(tokenRequest.uri, 186 | tokenRequest.headers.mapNotNull(), 187 | tokenRequest.formParameters.mapNotNull()) 188 | } 189 | 190 | private fun TokenResponse.asAccessToken() = when (this) { 191 | is TokenResponse.Success -> 192 | AccessToken(accessToken, expiresAt, scope) 193 | is TokenResponse.Error -> 194 | throw AuthException(error, description) 195 | } 196 | 197 | private fun TokenResponse.asRefreshToken() = when (this) { 198 | is TokenResponse.Success -> 199 | refreshToken ?: throw AuthException("refresh_failed", "Failed to request refresh token") 200 | is TokenResponse.Error -> 201 | throw AuthException(error, description) 202 | } 203 | 204 | } -------------------------------------------------------------------------------- /src/main/kotlin/org/jetbrains/hub/oauth2/client/jersey/JerseyClientTokenLoader.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.hub.oauth2.client.jersey 2 | 3 | import org.glassfish.jersey.client.JerseyClient 4 | import org.glassfish.jersey.client.JerseyClientBuilder 5 | import org.jetbrains.hub.oauth2.client.OAuth2Client 6 | import org.jetbrains.hub.oauth2.client.loader.TokenLoader 7 | import org.jetbrains.hub.oauth2.client.loader.TokenResponse 8 | import org.slf4j.LoggerFactory 9 | import java.net.URI 10 | import java.util.* 11 | import javax.ws.rs.client.Entity 12 | import javax.ws.rs.core.Form 13 | import javax.ws.rs.core.Response 14 | import javax.ws.rs.core.UriBuilder 15 | 16 | class JerseyClientTokenLoader(val jerseyClient: JerseyClient = JerseyClientBuilder.createClient()) : TokenLoader { 17 | val log = LoggerFactory.getLogger(JerseyClientTokenLoader::class.java) 18 | 19 | override fun load(uri: URI, headers: Map, formParameters: Map): TokenResponse { 20 | val request = jerseyClient.target(uri).request().let { request -> 21 | headers.entries.fold(request, { currentRequest, header -> 22 | currentRequest.header(header.key, header.value) 23 | }) 24 | } 25 | 26 | val requestTime = Calendar.getInstance() 27 | 28 | val response = request.post(Entity.form(Form().apply { 29 | formParameters.entries.forEach { 30 | param(it.key, it.value) 31 | } 32 | }), Response::class.java) 33 | 34 | return try { 35 | val entity = response.readEntity(Map::class.java) 36 | if (response.status == Response.Status.OK.statusCode) { 37 | TokenResponse.Success( 38 | entity["access_token"] as String, 39 | entity["refresh_token"] as? String, 40 | entity["expires_in"] as Int, 41 | requestTime, 42 | (entity["scope"] as? String)?.split(' ') ?: emptyList() 43 | ) 44 | } else { 45 | TokenResponse.Error( 46 | entity["error"] as String, 47 | entity["error_description"] as? String) 48 | } 49 | } catch (e: Exception) { 50 | log.info(e.message, e) 51 | TokenResponse.Error("unknown_error", e.message) 52 | } 53 | 54 | } 55 | 56 | override fun authURI(uri: URI, queryParameters: Map): URI { 57 | return UriBuilder.fromUri(uri).apply { 58 | queryParameters.keys.forEach { 59 | queryParam(it, "{$it}") 60 | } 61 | }.buildFromMap(queryParameters) 62 | } 63 | } 64 | 65 | fun oauth2Client() = OAuth2Client(JerseyClientTokenLoader()) -------------------------------------------------------------------------------- /src/main/kotlin/org/jetbrains/hub/oauth2/client/loader/AuthRequest.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.hub.oauth2.client.loader 2 | 3 | import org.jetbrains.hub.oauth2.client.BasicAuth 4 | import java.net.URI 5 | 6 | enum class GrantType(val value: String) { 7 | CLIENT_CREDENTIALS("client_credentials"), 8 | PASSWORD("password"), 9 | AUTHORIZATION_CODE("authorization_code"), 10 | REFRESH_TOKEN("refresh_token") 11 | } 12 | 13 | enum class ResponseType(val value: String) { 14 | TOKEN("token"), 15 | CODE("code") 16 | } 17 | 18 | enum class ClientAuthTransport() { HEADER, FORM } 19 | 20 | enum class PromptApproval(val value: String) { 21 | FORCE("force"), 22 | AUTO("auto") 23 | } 24 | 25 | enum class RequestCredentials(val value: String?) { 26 | /** 27 | * ``` 28 | * when 29 | * already logged in -> return logged in user 30 | * else -> show login form 31 | * ``` 32 | */ 33 | DEFAULT(null), 34 | /** 35 | * Log out currently logged in user, and show login form 36 | */ 37 | REQUIRED("required"), 38 | /** 39 | * ``` 40 | * when 41 | * already logged in -> return logged in user 42 | * guest is banned -> show login form 43 | * else -> return guest 44 | * ``` 45 | */ 46 | SKIP("skip"), 47 | /** 48 | * ``` 49 | * when 50 | * already logged in -> return logged in user 51 | * guest is banned -> return nothing 52 | * else -> return guest 53 | * ``` 54 | */ 55 | SILENT("silent"), 56 | /** 57 | * Log out currently logged in user, and return back 58 | */ 59 | SILENT_LOGOUT("silent_logout") 60 | } 61 | 62 | enum class AccessType(val value: String?) { 63 | /** 64 | * Don't request refresh token 65 | */ 66 | ONLINE(null), 67 | /** 68 | * Request refresh token 69 | */ 70 | OFFLINE("offline") 71 | } 72 | 73 | 74 | internal class AuthRequest(val uri: URI, var clientID: String) { 75 | var authResponseType: ResponseType? = null 76 | var state: String? = null 77 | var redirectURI: URI? = null 78 | var message: String? = null 79 | var requestRefreshToken: Boolean = false 80 | var prompt: PromptApproval? = null 81 | var requestCredentials: RequestCredentials = RequestCredentials.DEFAULT 82 | var scope: List? = null 83 | 84 | val queryParameters: Sequence> 85 | get() = sequenceOf( 86 | "response_type" to authResponseType?.value, 87 | "client_id" to clientID, 88 | "redirect_uri" to redirectURI?.toASCIIString(), 89 | "scope" to scope?.joinToString(" "), 90 | "state" to state, 91 | "message" to message, 92 | "approval_prompt" to prompt?.value?.toLowerCase(), 93 | "request_credentials" to requestCredentials.value?.toLowerCase(), 94 | "access_type" to if (requestRefreshToken) { 95 | AccessType.OFFLINE 96 | } else { 97 | AccessType.ONLINE 98 | }.value 99 | ) 100 | } 101 | 102 | internal class TokenRequest(val uri: URI, val clientID: String, val clientSecret: String) { 103 | var grantType: GrantType? = null 104 | var authTransport: ClientAuthTransport = ClientAuthTransport.HEADER 105 | var username: String? = null 106 | var password: String? = null 107 | var code: String? = null 108 | var refreshToken: String? = null 109 | var redirectURI: URI? = null 110 | var requestRefreshToken: Boolean = false 111 | var scope: List? = null 112 | 113 | 114 | val headers: Sequence> 115 | get() { 116 | val acceptHeader = sequenceOf("Accept" to "application/json") 117 | return if (authTransport == ClientAuthTransport.HEADER) { 118 | val clientID = clientID 119 | val basicAuthHeader = BasicAuth(clientID, clientSecret).header 120 | acceptHeader + sequenceOf(basicAuthHeader.name to basicAuthHeader.value) 121 | } else { 122 | acceptHeader 123 | } 124 | } 125 | 126 | val formParameters: Sequence> 127 | get() = sequenceOf( 128 | "grant_type" to grantType?.value, 129 | "client_id" to if (authTransport == ClientAuthTransport.FORM) clientID else null, 130 | "client_secret" to if (authTransport == ClientAuthTransport.FORM) clientSecret else null, 131 | "username" to username, 132 | "password" to password, 133 | "code" to code, 134 | "refresh_token" to refreshToken, 135 | "scope" to scope?.joinToString(" "), 136 | "redirect_uri" to redirectURI?.toASCIIString(), 137 | "access_type" to if (requestRefreshToken) { 138 | AccessType.OFFLINE 139 | } else { 140 | AccessType.ONLINE 141 | }.value 142 | ) 143 | 144 | } -------------------------------------------------------------------------------- /src/main/kotlin/org/jetbrains/hub/oauth2/client/loader/TokenLoader.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.hub.oauth2.client.loader 2 | 3 | import java.net.URI 4 | 5 | interface TokenLoader { 6 | fun load(uri: URI, 7 | headers: Map, 8 | formParameters: Map): TokenResponse 9 | 10 | fun authURI(uri: URI, queryParameters: Map): URI 11 | } -------------------------------------------------------------------------------- /src/main/kotlin/org/jetbrains/hub/oauth2/client/loader/TokenResponse.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.hub.oauth2.client.loader 2 | 3 | import java.util.* 4 | 5 | sealed class TokenResponse() { 6 | class Success( 7 | val accessToken: String, 8 | val refreshToken: String?, 9 | val expiresIn: Int, 10 | val requestTime: Calendar, 11 | val scope: List) : TokenResponse() { 12 | 13 | val expiresAt by lazy { 14 | Calendar.getInstance().apply { 15 | timeInMillis = requestTime.timeInMillis + expiresIn * 1000 16 | } 17 | } 18 | 19 | override fun equals(other: Any?): Boolean { 20 | if (this === other) return true 21 | if (other !is Success) return false 22 | 23 | if (accessToken != other.accessToken) return false 24 | if (refreshToken != other.refreshToken) return false 25 | 26 | return true 27 | } 28 | 29 | override fun hashCode(): Int { 30 | var result = accessToken.hashCode() 31 | result = 31 * result + (refreshToken?.hashCode() ?: 0) 32 | return result 33 | } 34 | } 35 | 36 | class Error(var error: String, var description: String? = null) : TokenResponse() 37 | } -------------------------------------------------------------------------------- /src/test/kotlin/org/jetbrains/hub/oauth2/client/AccessTokenSourceSpekTrait.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.hub.oauth2.client 2 | 3 | import org.jetbrains.hub.oauth2.client.loader.ClientAuthTransport 4 | import org.jetbrains.hub.oauth2.client.loader.TokenResponse 5 | import org.jetbrains.spek.api.DescribeBody 6 | import java.util.* 7 | import kotlin.test.assertEquals 8 | import kotlin.test.assertTrue 9 | 10 | 11 | fun DescribeBody.itShouldBeRefreshableTokenSource( 12 | getFlow: OAuth2Client.(ClientAuthTransport) -> AccessTokenSource) { 13 | it("shouldn't access server unless token is requested") { 14 | val tokenLoader = createTokenSourceAndLoader(getFlow).second 15 | assertTrue(tokenLoader.loadRecords.isEmpty()) 16 | } 17 | 18 | it("should cache token unless it is expired") { 19 | val (source, tokenLoader) = createTokenSourceAndLoader(getFlow) 20 | source.accessToken 21 | source.accessToken 22 | assertEquals(1, tokenLoader.loadRecords.size) 23 | } 24 | 25 | it("should refresh token when it is expired") { 26 | val (source, tokenLoader) = createTokenSourceAndLoader(getFlow, TokenResponse.Success( 27 | accessToken = "access-token", 28 | refreshToken = null, 29 | expiresIn = 3600, 30 | requestTime = "2016-06-16 12:00:00".toCalendar(), 31 | scope = listOf()) 32 | ) 33 | 34 | source.accessToken 35 | source.accessToken 36 | 37 | assertEquals(2, tokenLoader.loadRecords.size) 38 | } 39 | } 40 | 41 | 42 | private fun createTokenSourceAndLoader( 43 | getAccessTokenSource: OAuth2Client.(ClientAuthTransport) -> AccessTokenSource, 44 | tokenResponse: TokenResponse = TokenResponse.Success( 45 | accessToken = "access-token", 46 | refreshToken = null, 47 | expiresIn = 3600, 48 | requestTime = Calendar.getInstance(), 49 | scope = listOf())): Pair { 50 | val tokenLoader = MockTokenLoader { tokenResponse } 51 | 52 | val tokenSource = OAuth2Client(tokenLoader).getAccessTokenSource(ClientAuthTransport.HEADER) 53 | return Pair(tokenSource, tokenLoader) 54 | } -------------------------------------------------------------------------------- /src/test/kotlin/org/jetbrains/hub/oauth2/client/AccessTokenSpekTrait.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.hub.oauth2.client 2 | 3 | import org.jetbrains.hub.oauth2.client.loader.ClientAuthTransport 4 | import org.jetbrains.hub.oauth2.client.loader.TokenResponse 5 | import org.jetbrains.spek.api.DescribeBody 6 | import java.net.URI 7 | import java.util.* 8 | import kotlin.test.assertEquals 9 | import kotlin.test.assertNull 10 | 11 | 12 | fun DescribeBody.itShouldBeValidTokenSource( 13 | expectedTokenEndpoint: URI, 14 | clientID: String, clientSecret: String, 15 | expectedFormParameters: Map, 16 | getAccessToken: OAuth2Client.(ClientAuthTransport) -> AccessToken) { 17 | it("should parse access token correctly") { 18 | val (accessToken, tokenLoader) = createTokenSourceAndLoader(getAccessToken, TokenResponse.Success( 19 | accessToken = "access-token", 20 | refreshToken = null, 21 | expiresIn = 3600, 22 | requestTime = "2016-06-16 12:00:00".toCalendar(), 23 | scope = listOf("0-0-0-0-0")) 24 | ) 25 | assertEquals("access-token", accessToken.accessToken) 26 | assertEquals("2016-06-16 13:00:00", accessToken.expiresAt.asString()) 27 | assertEquals(listOf("0-0-0-0-0"), accessToken.scope) 28 | 29 | assertEquals("Bearer access-token", accessToken.header.value) 30 | 31 | assertEquals(1, tokenLoader.loadRecords.size) 32 | } 33 | 34 | it("should call correct token endpoint") { 35 | requestToken({ getAccessToken(ClientAuthTransport.HEADER) }) { 36 | assertEquals(expectedTokenEndpoint, uri) 37 | } 38 | } 39 | 40 | it("should request token with correct form parameters") { 41 | requestToken({ getAccessToken(ClientAuthTransport.HEADER) }) { 42 | assertEquals(expectedFormParameters, this.formParameters) 43 | } 44 | } 45 | 46 | it("should pass credentials as header parameters if required") { 47 | requestToken({ getAccessToken(ClientAuthTransport.HEADER) }) { 48 | assertEquals("Basic ${Base64.encode("$clientID:$clientSecret".toByteArray())}", headers["Authorization"]) 49 | assertNull(formParameters["client_id"]) 50 | assertNull(formParameters["client_secret"]) 51 | } 52 | } 53 | 54 | it("should pass credentials as form parameters if required") { 55 | requestToken({ getAccessToken(ClientAuthTransport.FORM) }) { 56 | assertNull(headers["Authorization"]) 57 | assertEquals(clientID, formParameters["client_id"]) 58 | assertEquals(clientSecret, formParameters["client_secret"]) 59 | } 60 | } 61 | } 62 | 63 | private fun requestToken( 64 | getAccessToken: OAuth2Client.() -> AccessToken, 65 | assertTokenRequest: MockTokenLoader.Request.() -> Unit) { 66 | OAuth2Client(MockTokenLoader { 67 | assertTokenRequest() 68 | TokenResponse.Success( 69 | accessToken = "access-token", 70 | refreshToken = "refresh-token", 71 | expiresIn = 3600, 72 | requestTime = "2016-06-16 12:00:00".toCalendar(), 73 | scope = listOf("0-0-0-0-0")) 74 | 75 | }).getAccessToken() 76 | } 77 | 78 | private fun createTokenSourceAndLoader( 79 | getAccessToken: OAuth2Client.(ClientAuthTransport) -> AccessToken, 80 | tokenResponse: TokenResponse = TokenResponse.Success( 81 | accessToken = "access-token", 82 | refreshToken = null, 83 | expiresIn = 3600, 84 | requestTime = Calendar.getInstance(), 85 | scope = listOf())): Pair { 86 | val tokenLoader = MockTokenLoader { tokenResponse } 87 | 88 | val accessToken = OAuth2Client(tokenLoader).getAccessToken(ClientAuthTransport.HEADER) 89 | return Pair(accessToken, tokenLoader) 90 | } -------------------------------------------------------------------------------- /src/test/kotlin/org/jetbrains/hub/oauth2/client/AuthURISpekTrait.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.hub.oauth2.client 2 | 3 | import org.jetbrains.hub.oauth2.client.loader.ClientAuthTransport 4 | import org.jetbrains.hub.oauth2.client.loader.TokenResponse 5 | import org.jetbrains.spek.api.DescribeBody 6 | import java.net.URI 7 | import java.util.* 8 | import kotlin.test.assertEquals 9 | import kotlin.test.assertNull 10 | 11 | 12 | fun DescribeBody.itShouldBeValidRefreshTokenSource( 13 | expectedTokenEndpoint: URI, 14 | clientID: String, clientSecret: String, 15 | expectedFormParameters: Map, 16 | getRefreshToken: OAuth2Client.(ClientAuthTransport) -> String) { 17 | it("should parse refresh token correctly") { 18 | val (refreshToken, tokenLoader) = createTokenSourceAndLoader(getRefreshToken, TokenResponse.Success( 19 | accessToken = "access-token", 20 | refreshToken = "refresh-token", 21 | expiresIn = 3600, 22 | requestTime = "2016-06-16 12:00:00".toCalendar(), 23 | scope = listOf("0-0-0-0-0")) 24 | ) 25 | assertEquals("refresh-token", refreshToken) 26 | 27 | assertEquals(1, tokenLoader.loadRecords.size) 28 | } 29 | 30 | it("should call correct token endpoint") { 31 | requestToken({ getRefreshToken(ClientAuthTransport.HEADER) }) { 32 | assertEquals(expectedTokenEndpoint, uri) 33 | } 34 | } 35 | 36 | it("should request token with correct form parameters") { 37 | requestToken({ getRefreshToken(ClientAuthTransport.HEADER) }) { 38 | assertEquals(expectedFormParameters, this.formParameters) 39 | } 40 | } 41 | 42 | it("should pass credentials as header parameters if required") { 43 | requestToken({ getRefreshToken(ClientAuthTransport.HEADER) }) { 44 | assertEquals("Basic ${Base64.encode("$clientID:$clientSecret".toByteArray())}", headers["Authorization"]) 45 | assertNull(formParameters["client_id"]) 46 | assertNull(formParameters["client_secret"]) 47 | } 48 | } 49 | 50 | it("should pass credentials as form parameters if required") { 51 | requestToken({ getRefreshToken(ClientAuthTransport.FORM) }) { 52 | assertNull(headers["Authorization"]) 53 | assertEquals(clientID, formParameters["client_id"]) 54 | assertEquals(clientSecret, formParameters["client_secret"]) 55 | } 56 | } 57 | 58 | it("shouldn't access server unless token is requested") { 59 | val tokenLoader = createTokenSourceAndLoader(getRefreshToken).second 60 | assertEquals(1, tokenLoader.loadRecords.size) 61 | } 62 | } 63 | 64 | private fun requestToken( 65 | getRefreshToken: OAuth2Client.() -> String, 66 | assertTokenRequest: MockTokenLoader.Request.() -> Unit) { 67 | OAuth2Client(MockTokenLoader { 68 | assertTokenRequest() 69 | TokenResponse.Success( 70 | accessToken = "access-token", 71 | refreshToken = "refresh-token", 72 | expiresIn = 3600, 73 | requestTime = "2016-06-16 12:00:00".toCalendar(), 74 | scope = listOf("0-0-0-0-0")) 75 | 76 | }).getRefreshToken() 77 | } 78 | 79 | private fun createTokenSourceAndLoader( 80 | getRefreshToken: OAuth2Client.(ClientAuthTransport) -> String, 81 | tokenResponse: TokenResponse = TokenResponse.Success( 82 | accessToken = "access-token", 83 | refreshToken = "refresh-token", 84 | expiresIn = 3600, 85 | requestTime = Calendar.getInstance(), 86 | scope = listOf())): Pair { 87 | val tokenLoader = MockTokenLoader { tokenResponse } 88 | 89 | val flow = OAuth2Client(tokenLoader).getRefreshToken(ClientAuthTransport.HEADER) 90 | return Pair(flow, tokenLoader) 91 | } -------------------------------------------------------------------------------- /src/test/kotlin/org/jetbrains/hub/oauth2/client/Base64Spek.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.hub.oauth2.client 2 | 3 | import org.jetbrains.spek.api.Spek 4 | import kotlin.test.assertEquals 5 | 6 | class Base64Spek : Spek({ 7 | describe("Base64") { 8 | on("encode") { 9 | given("empty array") { 10 | it("should return empty") { 11 | assertEquals("", Base64.encode(byteArrayOf())) 12 | } 13 | } 14 | 15 | given("some byte array") { 16 | val string = "1234-3213-3123:topsecret".toByteArray() 17 | it("should return it base64 encoded") { 18 | assertEquals("MTIzNC0zMjEzLTMxMjM6dG9wc2VjcmV0", Base64.encode(string)) 19 | } 20 | } 21 | 22 | given("some byte array of length mod 3 equal 1") { 23 | val string = "1234-3213-3123:topsecr".toByteArray() 24 | it("should pad it with ==") { 25 | assertEquals("MTIzNC0zMjEzLTMxMjM6dG9wc2Vjcg==", Base64.encode(string)) 26 | } 27 | } 28 | 29 | given("some byte array of length mod 3 equal 2") { 30 | val string = "1234-3213-3123:topsecre".toByteArray() 31 | it("should pad it with =") { 32 | assertEquals("MTIzNC0zMjEzLTMxMjM6dG9wc2VjcmU=", Base64.encode(string)) 33 | } 34 | } 35 | } 36 | } 37 | }) -------------------------------------------------------------------------------- /src/test/kotlin/org/jetbrains/hub/oauth2/client/ClientFlowSpek.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.hub.oauth2.client 2 | 3 | import org.jetbrains.hub.oauth2.client.loader.ClientAuthTransport 4 | import org.jetbrains.spek.api.Spek 5 | import java.net.URI 6 | 7 | class ClientFlowSpek : Spek({ 8 | describe("Token source") { 9 | val tokenEndpoint = URI.create("https://hub.jetbrains.com/api/rest/oauth2/token") 10 | val clientID = "1234-3213-3123" 11 | val clientSecret = "topsecret" 12 | val scopeElement = "0-0-0-0-0" 13 | val scope = listOf(scopeElement, clientID) 14 | 15 | val getFlow: OAuth2Client.(ClientAuthTransport) -> AccessTokenSource = { authTransport -> 16 | clientFlow(tokenEndpoint, clientID, clientSecret, scope, authTransport) 17 | } 18 | 19 | itShouldBeValidTokenSource(tokenEndpoint, clientID, clientSecret, mapOf( 20 | "grant_type" to "client_credentials", 21 | "scope" to "$scopeElement $clientID" 22 | ), { getFlow(it).accessToken }) 23 | 24 | itShouldBeRefreshableTokenSource(getFlow) 25 | } 26 | }) -------------------------------------------------------------------------------- /src/test/kotlin/org/jetbrains/hub/oauth2/client/CodeFlowSpek.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.hub.oauth2.client 2 | 3 | import org.jetbrains.hub.oauth2.client.jersey.oauth2Client 4 | import org.jetbrains.hub.oauth2.client.loader.ClientAuthTransport 5 | import org.jetbrains.spek.api.Spek 6 | import java.net.URI 7 | import kotlin.test.assertEquals 8 | 9 | class CodeFlowSpek : Spek({ 10 | describe("Authentication URI") { 11 | val authEndpoint = URI.create("https://hub.jetbrains.com/api/rest/oauth2/auth") 12 | val clientID = "1234-3213-3123" 13 | val redirectURI = URI.create("https://localhost:8080") 14 | val scopeElement = "0-0-0-0-0" 15 | val scope = listOf(scopeElement, clientID) 16 | val state = "some-unique-state-id" 17 | 18 | val authClient = OAuth2Client(MockTokenLoader { throw UnsupportedOperationException() }) 19 | 20 | it("should form correct URI for online access token request") { 21 | assertEquals( 22 | "https://hub.jetbrains.com/api/rest/oauth2/auth" + 23 | "?response_type=code" + 24 | "&client_id=$clientID" + 25 | "&redirect_uri=https%3A%2F%2Flocalhost%3A8080" + 26 | "&scope=$scopeElement+$clientID" + 27 | "&state=$state", 28 | authClient.codeFlowURI( 29 | authEndpoint, 30 | clientID, redirectURI, 31 | scope, state, requestRefreshToken = false).toASCIIString() 32 | ) 33 | } 34 | 35 | it("should form correct for URI offline access token request") { 36 | assertEquals( 37 | "https://hub.jetbrains.com/api/rest/oauth2/auth" + 38 | "?response_type=code" + 39 | "&client_id=$clientID" + 40 | "&redirect_uri=https%3A%2F%2Flocalhost%3A8080" + 41 | "&scope=$scopeElement+$clientID" + 42 | "&state=$state" + 43 | "&access_type=offline", 44 | authClient.codeFlowURI( 45 | authEndpoint, 46 | clientID, redirectURI, 47 | scope, state, requestRefreshToken = true).toASCIIString() 48 | ) 49 | } 50 | 51 | it("should form correct for URI offline access token request with JerseyClient") { 52 | assertEquals( 53 | "https://hub.jetbrains.com/api/rest/oauth2/auth" + 54 | "?response_type=code" + 55 | "&client_id=$clientID" + 56 | "&redirect_uri=https%3A%2F%2Flocalhost%3A8080" + 57 | "&scope=$scopeElement+$clientID" + 58 | "&state=$state" + 59 | "&access_type=offline", 60 | oauth2Client().codeFlowURI( 61 | authEndpoint, 62 | clientID, redirectURI, 63 | scope, state, requestRefreshToken = true).toASCIIString() 64 | ) 65 | } 66 | } 67 | 68 | describe("Token source") { 69 | val tokenEndpoint = URI.create("https://hub.jetbrains.com/api/rest/oauth2/token") 70 | val clientID = "1234-3213-3123" 71 | val clientSecret = "topsecret" 72 | val code = "SOME-CODE" 73 | val redirectURI = URI.create("https://localhost:8080") 74 | 75 | val getAccessToken: OAuth2Client.(ClientAuthTransport) -> AccessToken = { authTransport -> 76 | codeFlow(tokenEndpoint, code, redirectURI, clientID, clientSecret, authTransport) 77 | } 78 | 79 | itShouldBeValidTokenSource(tokenEndpoint, clientID, clientSecret, mapOf( 80 | "grant_type" to "authorization_code", 81 | "code" to code, 82 | "redirect_uri" to redirectURI.toASCIIString() 83 | ), getAccessToken) 84 | } 85 | 86 | describe("Refresh token") { 87 | val tokenEndpoint = URI.create("https://hub.jetbrains.com/api/rest/oauth2/token") 88 | val clientID = "1234-3213-3123" 89 | val clientSecret = "topsecret" 90 | val code = "SOME-CODE" 91 | val redirectURI = URI.create("https://localhost:8080") 92 | 93 | itShouldBeValidRefreshTokenSource(tokenEndpoint, clientID, clientSecret, mapOf( 94 | "grant_type" to "authorization_code", 95 | "code" to code, 96 | "redirect_uri" to redirectURI.toASCIIString() 97 | )) { authTransport -> 98 | codeRefreshToken(tokenEndpoint, code, redirectURI, clientID, clientSecret, authTransport) 99 | } 100 | } 101 | 102 | }) -------------------------------------------------------------------------------- /src/test/kotlin/org/jetbrains/hub/oauth2/client/DateUtil.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.hub.oauth2.client 2 | 3 | import java.text.SimpleDateFormat 4 | import java.util.* 5 | 6 | val simpleDateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss") 7 | fun String.toCalendar() = Calendar.getInstance().apply { 8 | time = simpleDateFormat.parse(this@toCalendar) 9 | } 10 | 11 | fun Calendar.asString() = simpleDateFormat.format(time) 12 | -------------------------------------------------------------------------------- /src/test/kotlin/org/jetbrains/hub/oauth2/client/ImplicitFlowSpek.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.hub.oauth2.client 2 | 3 | import org.jetbrains.spek.api.Spek 4 | import java.net.URI 5 | import kotlin.test.assertEquals 6 | 7 | class ImplicitFlowSpek : Spek({ 8 | describe("Authentication URI") { 9 | val authEndpoint = URI.create("https://hub.jetbrains.com/api/rest/oauth2/auth") 10 | val clientID = "1234-3213-3123" 11 | val redirectURI = URI.create("https://localhost:8080") 12 | val scopeElement = "0-0-0-0-0" 13 | val scope = listOf(scopeElement, clientID) 14 | val state = "some-unique-state-id" 15 | 16 | val authClient = OAuth2Client(MockTokenLoader { throw UnsupportedOperationException() }) 17 | 18 | it("should form correct URI") { 19 | assertEquals( 20 | "https://hub.jetbrains.com/api/rest/oauth2/auth" + 21 | "?response_type=token" + 22 | "&client_id=$clientID" + 23 | "&redirect_uri=https%3A%2F%2Flocalhost%3A8080" + 24 | "&scope=$scopeElement+$clientID" + 25 | "&state=$state", 26 | authClient.implicitFlowURI( 27 | authEndpoint, 28 | clientID, redirectURI, 29 | scope, state).toASCIIString() 30 | ) 31 | } 32 | } 33 | }) -------------------------------------------------------------------------------- /src/test/kotlin/org/jetbrains/hub/oauth2/client/MockTokenLoader.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.hub.oauth2.client 2 | 3 | import org.jetbrains.hub.oauth2.client.loader.TokenLoader 4 | import org.jetbrains.hub.oauth2.client.loader.TokenResponse 5 | import java.net.URI 6 | import java.net.URLEncoder 7 | import java.util.* 8 | 9 | open class MockTokenLoader(var onTokenRequest: Request.() -> TokenResponse) : TokenLoader { 10 | val loadRecords = ArrayList() 11 | 12 | override fun load(uri: URI, headers: Map, formParameters: Map): TokenResponse { 13 | val request = Request(uri, headers, formParameters) 14 | loadRecords.add(request) 15 | return request.onTokenRequest() 16 | } 17 | 18 | override fun authURI(uri: URI, queryParameters: Map): URI { 19 | return URI.create("${uri.toASCIIString()}?${queryParameters.map { 20 | val eName = URLEncoder.encode(it.key, "UTF-8") 21 | val eValue = URLEncoder.encode(it.value, "UTF-8") 22 | "$eName=$eValue" 23 | }.joinToString("&")}") 24 | } 25 | 26 | class Request(val uri: URI, val headers: Map, val formParameters: Map) 27 | } -------------------------------------------------------------------------------- /src/test/kotlin/org/jetbrains/hub/oauth2/client/RefreshTokenFlowSpek.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.hub.oauth2.client 2 | 3 | import org.jetbrains.hub.oauth2.client.loader.ClientAuthTransport 4 | import org.jetbrains.spek.api.Spek 5 | import java.net.URI 6 | 7 | class RefreshTokenFlowSpek : Spek({ 8 | describe("Token source") { 9 | val tokenEndpoint = URI.create("https://hub.jetbrains.com/api/rest/oauth2/token") 10 | val clientID = "1234-3213-3123" 11 | val clientSecret = "topsecret" 12 | val refreshToken = "SOME-CODE" 13 | val scopeElement = "0-0-0-0-0" 14 | val scope = listOf(scopeElement, clientID) 15 | 16 | val getFlow: OAuth2Client.(ClientAuthTransport) -> AccessTokenSource = { authTransport -> 17 | refreshTokenFlow(tokenEndpoint, refreshToken, clientID, clientSecret, scope, authTransport) 18 | } 19 | 20 | itShouldBeValidTokenSource(tokenEndpoint, clientID, clientSecret, mapOf( 21 | "grant_type" to "refresh_token", 22 | "refresh_token" to refreshToken, 23 | "scope" to "$scopeElement $clientID" 24 | ), { getFlow(it).accessToken }) 25 | 26 | itShouldBeRefreshableTokenSource(getFlow) 27 | } 28 | }) -------------------------------------------------------------------------------- /src/test/kotlin/org/jetbrains/hub/oauth2/client/ResourceOwnerFlowSpek.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.hub.oauth2.client 2 | 3 | import org.jetbrains.hub.oauth2.client.loader.ClientAuthTransport 4 | import org.jetbrains.spek.api.Spek 5 | import java.net.URI 6 | 7 | class ResourceOwnerFlowSpek : Spek({ 8 | describe("Token source") { 9 | val tokenEndpoint = URI.create("https://hub.jetbrains.com/api/rest/oauth2/token") 10 | val clientID = "1234-3213-3123" 11 | val clientSecret = "topsecret" 12 | val username = "user" 13 | val password = "secret" 14 | val scopeElement = "0-0-0-0-0" 15 | val scope = listOf(scopeElement, clientID) 16 | 17 | val getFlow: OAuth2Client.(ClientAuthTransport) -> AccessTokenSource = { authTransport -> 18 | resourceOwnerFlow(tokenEndpoint, username, password, clientID, clientSecret, scope, authTransport) 19 | } 20 | 21 | itShouldBeValidTokenSource(tokenEndpoint, clientID, clientSecret, mapOf( 22 | "grant_type" to "password", 23 | "username" to username, 24 | "password" to password, 25 | "scope" to "$scopeElement $clientID" 26 | ), { getFlow(it).accessToken }) 27 | 28 | itShouldBeRefreshableTokenSource(getFlow) 29 | } 30 | 31 | describe("Refresh token") { 32 | val tokenEndpoint = URI.create("https://hub.jetbrains.com/api/rest/oauth2/token") 33 | val clientID = "1234-3213-3123" 34 | val clientSecret = "topsecret" 35 | val username = "user" 36 | val password = "secret" 37 | val scopeElement = "0-0-0-0-0" 38 | val scope = listOf(scopeElement, clientID) 39 | 40 | 41 | itShouldBeValidRefreshTokenSource(tokenEndpoint, clientID, clientSecret, mapOf( 42 | "grant_type" to "password", 43 | "username" to username, 44 | "password" to password, 45 | "scope" to "$scopeElement $clientID", 46 | "access_type" to "offline" 47 | )) { authTransport -> 48 | resourceOwnerRefreshToken(tokenEndpoint, username, password, clientID, clientSecret, scope, authTransport) 49 | } 50 | } 51 | }) -------------------------------------------------------------------------------- /src/test/kotlin/org/jetbrains/hub/oauth2/client/sample/Hub.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.hub.oauth2.client.sample 2 | 3 | import org.jetbrains.hub.oauth2.client.jersey.oauth2Client 4 | import java.net.URI 5 | 6 | fun main(args: Array) { 7 | val client = oauth2Client().clientFlow( 8 | URI("https://hub-staging.labs.intellij.net/api/rest/oauth2/token"), 9 | "client", 10 | "secret", 11 | listOf("0-0-0-0-0")) 12 | 13 | println(client.accessToken) 14 | } --------------------------------------------------------------------------------