Skip to content

Commit f69ae3b

Browse files
committed
Provide method to encode/decode a OAuth2InteractiveGrant to/from String, closes #92
1 parent 9b74b5b commit f69ae3b

20 files changed

+460
-34
lines changed

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,19 @@ String authorization = String.format("Bearer %s", token.accessToken());
124124
myConnection.setRequestProperty("Authorization", authorization);
125125
```
126126

127+
### Preserving state
128+
129+
By design, an interactive grant (Authorization Code Grant and Implicit Grant) may require multiple round trips. Sometimes an application
130+
needs to preserve the state between these round trips. For instance, an Android app may be suspended while the browser is in the foreground, and it may
131+
have to preserve the grant state in order to receive the access token when it's resumed.
132+
133+
This can be achieved by calling `encodedState()` on the `OAuth2InteractiveGrant` instance. This method returns a String that can later be used
134+
to restore the grant state with
135+
136+
```java
137+
OAuth2InteractiveGrant grant = new InteractiveGrantFactory(oauth2Client).value(encodedState);
138+
```
139+
127140
## Choice of HTTP client
128141

129142
This library doesn't depend on any specific HTTP client implementation. Instead it builds upon [http-client-essentials-suite](https://github.com/dmfs/http-client-essentials-suite) to allow any 3rd party HTTP client to be used.

build.gradle

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ if (project.hasProperty('SONATYPE_USERNAME') && project.hasProperty('SONATYPE_PA
9696
}
9797

9898
dependencies {
99-
api 'org.dmfs:jems:' + JEMS_VERSION
99+
api 'org.dmfs:jems:1.44'
100100
api 'org.dmfs:rfc3986-uri:0.8.1'
101101
api 'org.dmfs:rfc5545-datetime:0.3'
102102
api 'org.dmfs:http-client-essentials:' + HTTP_CLIENT_ESSENTIALS_VERSION
@@ -106,9 +106,13 @@ dependencies {
106106
implementation 'org.dmfs:http-client-types:' + HTTP_CLIENT_ESSENTIALS_VERSION
107107
implementation 'org.dmfs:http-client-basics:' + HTTP_CLIENT_ESSENTIALS_VERSION
108108
implementation 'org.dmfs:http-executor-decorators:' + HTTP_CLIENT_ESSENTIALS_VERSION
109+
implementation 'org.dmfs:express-json:0.2.0'
110+
implementation 'org.dmfs:jems2:' + JEMS2_VERSION
109111
implementation 'org.json:json:20220924'
110112

111-
testImplementation 'org.dmfs:jems-testing:' + JEMS_VERSION
113+
testImplementation 'org.saynotobugs:confidence-core:0.8.0'
114+
testImplementation 'org.saynotobugs:confidence-mockito4:0.8.0'
115+
testImplementation 'org.dmfs:jems2-testing:' + JEMS2_VERSION
112116
testImplementation 'junit:junit:4.13.2'
113117
testImplementation 'org.mockito:mockito-core:4.8.0'
114118
testImplementation 'org.hamcrest:hamcrest-library:2.2'

gradle.properties

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
2-
31
#
42
# Copyright 2016 dmfs GmbH
53
#
@@ -15,10 +13,8 @@
1513
# See the License for the specific language governing permissions and
1614
# limitations under the License.
1715
#
18-
1916
HTTP_CLIENT_ESSENTIALS_VERSION=0.20
20-
JEMS_VERSION=1.41
21-
17+
JEMS2_VERSION=2.14.0
2218
POM_DEVELOPER_ID=dmfs
2319
POM_DEVELOPER_NAME=Marten Gajda
2420
POM_DEVELOPER_EMAIL=[email protected]

src/main/java/org/dmfs/oauth2/client/OAuth2InteractiveGrant.java

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import org.dmfs.httpessentials.exceptions.ProtocolError;
2121
import org.dmfs.httpessentials.exceptions.ProtocolException;
2222
import org.dmfs.rfc3986.Uri;
23+
import org.json.JSONArray;
2324

2425
import java.io.Serializable;
2526
import java.net.URI;
@@ -39,7 +40,7 @@ public interface OAuth2InteractiveGrant extends OAuth2Grant
3940
*
4041
* @return A {@link URI}.
4142
*/
42-
public URI authorizationUrl();
43+
URI authorizationUrl();
4344

4445
/**
4546
* Update the authentication flow with the redirect URI that was returned by the user agent. Unless this throws an Exception, the caller can assume that
@@ -55,7 +56,7 @@ public interface OAuth2InteractiveGrant extends OAuth2Grant
5556
* @throws ProtocolException
5657
* If the redirectUri is invalid.
5758
*/
58-
public OAuth2InteractiveGrant withRedirect(Uri redirectUri) throws ProtocolError, ProtocolException;
59+
OAuth2InteractiveGrant withRedirect(Uri redirectUri) throws ProtocolError, ProtocolException;
5960

6061
/**
6162
* Return a {@link Serializable} state object that can be used to retain the current authentication flow state whenever the original {@link
@@ -68,13 +69,34 @@ public interface OAuth2InteractiveGrant extends OAuth2Grant
6869
*
6970
* @throws UnsupportedOperationException
7071
* if this grant type does not support exporting the current state.
72+
* @deprecated in favour of {@link #encodedState()}.
7173
*/
72-
public OAuth2GrantState state() throws UnsupportedOperationException;
74+
@Deprecated
75+
OAuth2GrantState state() throws UnsupportedOperationException;
76+
77+
/**
78+
* Returns a {@link String} that can be later used to retrieve an {@link OAuth2InteractiveGrant} with the same state.
79+
* <p>
80+
* Note, the format of the String may be changed without further notice and may be incompatible with future versions of this library.
81+
* <p>
82+
* Also note, the resulting String may contain secrets used during the interactive grant. Do not persist the result in its plain
83+
* text form. Make sure to encrypt it with a secure cipher.
84+
* <p>
85+
* Retrieve the original {@link OAuth2InteractiveGrant} like this:
86+
* <pre>{@code
87+
* InteractiveGrant grant = new InteractiveGrantFactory(oauth2Client).value(encodedState);
88+
* }
89+
* </pre>
90+
*/
91+
String encodedState();
7392

7493
/**
7594
* The interface of a simple {@link Serializable} object that represents the state of an interactive grant.
95+
*
96+
* @deprecated in favour of {@link OAuth2InteractiveGrant#encodedState()}.
7697
*/
77-
public interface OAuth2GrantState extends Serializable
98+
@Deprecated
99+
interface OAuth2GrantState extends Serializable
78100
{
79101
/**
80102
* Creates an {@link OAuth2InteractiveGrant} from this state for the given client.
@@ -86,6 +108,12 @@ public interface OAuth2GrantState extends Serializable
86108
*
87109
* @return An {@link OAuth2InteractiveGrant} that can be used to continue the authentication flow.
88110
*/
89-
public OAuth2InteractiveGrant grant(OAuth2Client client);
111+
OAuth2InteractiveGrant grant(OAuth2Client client);
112+
}
113+
114+
115+
interface OAuth2InteractiveGrantFactory
116+
{
117+
OAuth2InteractiveGrant grant(OAuth2Client client, JSONArray arguments);
90118
}
91119
}

src/main/java/org/dmfs/oauth2/client/grants/AuthorizationCodeGrant.java

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,22 +16,30 @@
1616

1717
package org.dmfs.oauth2.client.grants;
1818

19+
import net.iharder.Base64;
20+
21+
import org.dmfs.express.json.elementary.JsonText;
1922
import org.dmfs.httpessentials.client.HttpRequestExecutor;
2023
import org.dmfs.httpessentials.exceptions.ProtocolError;
2124
import org.dmfs.httpessentials.exceptions.ProtocolException;
2225
import org.dmfs.oauth2.client.*;
2326
import org.dmfs.oauth2.client.http.requests.AuthorizationCodeTokenRequest;
2427
import org.dmfs.oauth2.client.pkce.S256CodeChallenge;
2528
import org.dmfs.oauth2.client.scope.StringScope;
29+
import org.dmfs.oauth2.client.utils.GrantState;
2630
import org.dmfs.rfc3986.Uri;
2731
import org.dmfs.rfc3986.encoding.Precoded;
2832
import org.dmfs.rfc3986.encoding.XWwwFormUrlEncoded;
2933
import org.dmfs.rfc3986.parameters.ParameterList;
3034
import org.dmfs.rfc3986.parameters.adapters.XwfueParameterList;
3135
import org.dmfs.rfc3986.parameters.parametersets.EmptyParameterList;
36+
import org.dmfs.rfc3986.uris.LazyUri;
37+
import org.dmfs.rfc3986.uris.Text;
38+
import org.json.JSONArray;
3239

3340
import java.io.IOException;
3441
import java.net.URI;
42+
import java.nio.charset.StandardCharsets;
3543

3644

3745
/**
@@ -125,13 +133,44 @@ public OAuth2AccessToken accessToken(HttpRequestExecutor executor) throws IOExce
125133
}
126134

127135

136+
@Deprecated
128137
@Override
129138
public OAuth2InteractiveGrant.OAuth2GrantState state()
130139
{
131140
return new InitialAuthorizationCodeGrantState(mScope, mState, mCodeVerifier, new XWwwFormUrlEncoded(mCustomParameters));
132141
}
133142

134143

144+
@Override
145+
public String encodedState()
146+
{
147+
return Base64.encodeBytes(
148+
new JsonText(new GrantState(InitialAuthorizationCodeGrantFactory.class,
149+
mScope.toString(), mState.toString(), mCodeVerifier.toString(), new XWwwFormUrlEncoded(mCustomParameters).toString()))
150+
.value()
151+
.getBytes(StandardCharsets.UTF_8));
152+
}
153+
154+
155+
private final static class InitialAuthorizationCodeGrantFactory implements OAuth2InteractiveGrantFactory
156+
{
157+
158+
@Override
159+
public OAuth2InteractiveGrant grant(OAuth2Client client, JSONArray arguments)
160+
{
161+
if (arguments.length() != 4)
162+
{
163+
throw new IllegalArgumentException("Can't restore grant from invalid state.");
164+
}
165+
return new AuthorizationCodeGrant(client,
166+
new StringScope(arguments.getString(0)),
167+
arguments.getString(1),
168+
arguments.getString(2),
169+
new XwfueParameterList(new Precoded(arguments.getString(3))));
170+
}
171+
}
172+
173+
135174
/**
136175
* An {@link OAuth2InteractiveGrant} that represents the authorized state of an Authorization Code Grant. That means, the user has granted access and an
137176
* auth token was issued by the server.
@@ -181,17 +220,49 @@ public OAuth2InteractiveGrant withRedirect(Uri redirectUri)
181220
}
182221

183222

223+
@Deprecated
184224
@Override
185225
public OAuth2GrantState state()
186226
{
187227
return new AuthorizedAuthorizationCodeGrantState(mScope, mRedirectUri, mState, mCodeVerifier);
188228
}
229+
230+
231+
@Override
232+
public String encodedState()
233+
{
234+
return Base64.encodeBytes(
235+
new JsonText(new GrantState(AuthenticatedAuthorizationCodeGrantFactory.class,
236+
new Text(mRedirectUri).toString(), mScope.toString(), mState.toString(), mCodeVerifier.toString()))
237+
.value()
238+
.getBytes(StandardCharsets.UTF_8));
239+
}
240+
241+
242+
private final static class AuthenticatedAuthorizationCodeGrantFactory implements OAuth2InteractiveGrantFactory
243+
{
244+
245+
@Override
246+
public OAuth2InteractiveGrant grant(OAuth2Client client, JSONArray arguments)
247+
{
248+
if (arguments.length() != 4)
249+
{
250+
throw new IllegalArgumentException("Can't restore grant from invalid state.");
251+
}
252+
return new AuthorizedAuthorizationCodeGrant(client,
253+
new LazyUri(new Precoded(arguments.getString(0))),
254+
new StringScope(arguments.getString(1)),
255+
arguments.getString(2),
256+
arguments.getString(3));
257+
}
258+
}
189259
}
190260

191261

192262
/**
193263
* An {@link OAuth2GrantState} that represents the state of an Authorization Code Grant that was not confirmed by the user so far.
194264
*/
265+
@Deprecated
195266
private final static class InitialAuthorizationCodeGrantState implements OAuth2InteractiveGrant.OAuth2GrantState
196267
{
197268

@@ -229,6 +300,7 @@ public AuthorizationCodeGrant grant(OAuth2Client client)
229300
/**
230301
* An {@link OAuth2GrantState} that represents the state of an Authorization Code Grant that got user consent.
231302
*/
303+
@Deprecated
232304
private final static class AuthorizedAuthorizationCodeGrantState implements OAuth2InteractiveGrant.OAuth2GrantState
233305
{
234306
private static final long serialVersionUID = 1L;

0 commit comments

Comments
 (0)