├── .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 | [](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 | }
--------------------------------------------------------------------------------