Skip to content

Commit 981cdb1

Browse files
Merge branch 'release/0.2.0'
2 parents 6d145ff + abcca77 commit 981cdb1

18 files changed

+304
-93
lines changed

.idea/inspectionProfiles/Project_Default.xml

Lines changed: 0 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ support for [PKCE](https://datatracker.ietf.org/doc/html/rfc8252#section-8.1) an
1313
## Usage
1414

1515
```java
16-
// this library will just to the Authorization Flow:
16+
// this library will just perform the Authorization Flow:
1717
String tokenResponse = AuthFlow.asClient("oauth-client-id")
1818
.authorize(URI.create("https://login.example.com/oauth2/authorize"), uri -> System.out.println("Please login on " + uri))
1919
.getAccessToken(URI.create("https://login.example.com/oauth2/token"));
@@ -33,6 +33,6 @@ The `authorize(...)` method optionally allows you to specify:
3333
## Why this library?
3434

3535
* Often you just need to authorize your client and nothing more. Most OAuth2 libraries try to do a lot more
36-
* Nano-tiny-minuscule attack surface, since this doesn't contain any JOSE/JWT signature code, nor a fully-fleged web server
36+
* Nano-tiny-minuscule attack surface, since this doesn't contain any JOSE/JWT signature code, nor a fully-fledged web server
3737
* Focus is strictly on the authorization flow. Use any library for dealing with the tokens, you like.
3838
* Modular jar, exposing only one single public API. No need to read docs, you can't do anything wrong.

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<modelVersion>4.0.0</modelVersion>
66
<groupId>io.github.coffeelibs</groupId>
77
<artifactId>tiny-oauth2-client</artifactId>
8-
<version>0.1.1</version>
8+
<version>0.2.0</version>
99
<name>Tiny OAuth2 Client</name>
1010
<description>Zero Dependency RFC 8252 Authorization Flow</description>
1111
<inceptionYear>2022</inceptionYear>

src/main/java/io/github/coffeelibs/tinyoauth2client/AuthFlow.java

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package io.github.coffeelibs.tinyoauth2client;
22

33
import io.github.coffeelibs.tinyoauth2client.http.RedirectTarget;
4+
import io.github.coffeelibs.tinyoauth2client.http.Response;
45
import io.github.coffeelibs.tinyoauth2client.util.RandomUtil;
56
import org.jetbrains.annotations.Blocking;
7+
import org.jetbrains.annotations.Contract;
68
import org.jetbrains.annotations.VisibleForTesting;
79

810
import java.io.IOException;
@@ -32,6 +34,9 @@ public class AuthFlow {
3234
@VisibleForTesting
3335
final PKCE pkce;
3436

37+
private Response successResponse;
38+
private Response errorResponse;
39+
3540
private AuthFlow(String clientId) {
3641
this.clientId = clientId;
3742
this.pkce = new PKCE();
@@ -47,6 +52,54 @@ public static AuthFlow asClient(String clientId) {
4752
return new AuthFlow(clientId);
4853
}
4954

55+
/**
56+
* HTML to display in the Resource Owner's user agent after successful authorization.
57+
*
58+
* @param html content served with {@code Content-Type: text/html; charset=UTF-8}
59+
* @return this
60+
*/
61+
@Contract("_ -> this")
62+
public AuthFlow withSuccessHtml(String html) {
63+
this.successResponse = Response.html(Response.Status.OK, html);
64+
return this;
65+
}
66+
67+
/**
68+
* Where to redirect the Resource Owner's user agent after successful authorization.
69+
*
70+
* @param target URI of page to show
71+
* @return this
72+
*/
73+
@Contract("_ -> this")
74+
public AuthFlow withSuccessRedirect(URI target) {
75+
this.successResponse = Response.redirect(target);
76+
return this;
77+
}
78+
79+
/**
80+
* HTML to display in the Resource Owner's user agent after failed authorization.
81+
*
82+
* @param html content served with {@code Content-Type: text/html; charset=UTF-8}
83+
* @return this
84+
*/
85+
@Contract("_ -> this")
86+
public AuthFlow withErrorHtml(String html) {
87+
this.errorResponse = Response.html(Response.Status.OK, html);
88+
return this;
89+
}
90+
91+
/**
92+
* Where to redirect the Resource Owner's user agent after failed authorization.
93+
*
94+
* @param target URI of page to show
95+
* @return this
96+
*/
97+
@Contract("_ -> this")
98+
public AuthFlow withErrorRedirect(URI target) {
99+
this.errorResponse = Response.redirect(target);
100+
return this;
101+
}
102+
50103
/**
51104
* Asks the given {@code browser} to browse the authorization URI. This method will block until the browser is
52105
* <a href="https://datatracker.ietf.org/doc/html/rfc8252#section-4.1">redirected back to this application</a>.
@@ -58,6 +111,7 @@ public static AuthFlow asClient(String clientId) {
58111
* @throws IOException In case of I/O errors during communication between browser and this application
59112
* @see #authorize(URI, Consumer, Set, String, int...)
60113
*/
114+
@Blocking
61115
public AuthFlowWithCode authorize(URI authEndpoint, Consumer<URI> browser, String... scopes) throws IOException {
62116
return authorize(authEndpoint, browser, Set.of(scopes), "/" + RandomUtil.randomToken(16));
63117
}
@@ -77,6 +131,12 @@ public AuthFlowWithCode authorize(URI authEndpoint, Consumer<URI> browser, Strin
77131
@Blocking
78132
public AuthFlowWithCode authorize(URI authEndpoint, Consumer<URI> browser, Set<String> scopes, String path, int... ports) throws IOException {
79133
try (var redirectTarget = RedirectTarget.start(path, ports)) {
134+
if (successResponse != null) {
135+
redirectTarget.setSuccessResponse(successResponse);
136+
}
137+
if (errorResponse != null) {
138+
redirectTarget.setErrorResponse(errorResponse);
139+
}
80140
var encodedRedirectUri = URLEncoder.encode(redirectTarget.getRedirectUri().toASCIIString(), StandardCharsets.US_ASCII);
81141

82142
// https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1
@@ -107,8 +167,8 @@ public AuthFlowWithCode authorize(URI authEndpoint, Consumer<URI> browser, Set<S
107167
* The successfully authenticated authentication flow, ready to retrieve an access token.
108168
*/
109169
public class AuthFlowWithCode {
110-
private String encodedRedirectUri;
111-
private String authorizationCode;
170+
private final String encodedRedirectUri;
171+
private final String authorizationCode;
112172

113173
@VisibleForTesting
114174
AuthFlowWithCode(String encodedRedirectUri, String authorizationCode) {

src/main/java/io/github/coffeelibs/tinyoauth2client/http/HttpEmptyResponse.java renamed to src/main/java/io/github/coffeelibs/tinyoauth2client/http/EmptyResponse.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
import java.io.IOException;
44
import java.io.Writer;
55

6-
class HttpEmptyResponse implements HttpResponse {
6+
class EmptyResponse implements Response {
77

88
private final Status status;
99

10-
public HttpEmptyResponse(HttpResponse.Status status) {
10+
public EmptyResponse(Response.Status status) {
1111
this.status = status;
1212
}
1313

src/main/java/io/github/coffeelibs/tinyoauth2client/http/HttpHtmlResponse.java renamed to src/main/java/io/github/coffeelibs/tinyoauth2client/http/HtmlResponse.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44
import java.io.Writer;
55
import java.nio.charset.StandardCharsets;
66

7-
class HttpHtmlResponse implements HttpResponse {
7+
class HtmlResponse implements Response {
88

99
private final Status status;
1010
private final String body;
1111

12-
public HttpHtmlResponse(Status status, String body) {
12+
public HtmlResponse(Status status, String body) {
1313
this.status = status;
1414
this.body = body;
1515
}

src/main/java/io/github/coffeelibs/tinyoauth2client/http/InvalidRequestException.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
package io.github.coffeelibs.tinyoauth2client.http;
22

33
public class InvalidRequestException extends Exception {
4-
public final HttpResponse suggestedResponse;
4+
public final Response suggestedResponse;
55

6-
public InvalidRequestException(HttpResponse suggestedResponse) {
6+
public InvalidRequestException(Response suggestedResponse) {
77
this.suggestedResponse = suggestedResponse;
88
}
99

src/main/java/io/github/coffeelibs/tinyoauth2client/http/HttpRedirectResponse.java renamed to src/main/java/io/github/coffeelibs/tinyoauth2client/http/RedirectResponse.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44
import java.io.Writer;
55
import java.net.URI;
66

7-
class HttpRedirectResponse implements HttpResponse {
7+
class RedirectResponse implements Response {
88

99
private final Status status;
1010
private URI target;
1111

12-
public HttpRedirectResponse(Status status, URI target) {
12+
public RedirectResponse(Status status, URI target) {
1313
this.status = status;
1414
this.target = target;
1515
}

src/main/java/io/github/coffeelibs/tinyoauth2client/http/RedirectTarget.java

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import java.nio.channels.ServerSocketChannel;
1717
import java.nio.charset.StandardCharsets;
1818
import java.nio.file.Path;
19+
import java.util.Objects;
1920

2021
/**
2122
* A TCP connector to deal with the redirect response. We only listen for the expected response,
@@ -36,7 +37,8 @@ public class RedirectTarget implements Closeable {
3637
private final String path;
3738
private final String csrfToken;
3839

39-
private HttpResponse successResponse = HttpResponse.html(HttpResponse.Status.OK, "<html><body>Success</body></html>"); // TODO: allow customization with custom html or redirect
40+
private Response successResponse = Response.html(Response.Status.OK, "<html><body>Success</body></html>");
41+
private Response errorResponse = Response.html(Response.Status.OK, "<html><body>Error</body></html>");
4042

4143
private RedirectTarget(ServerSocketChannel serverChannel, String path) {
4244
this.serverChannel = serverChannel;
@@ -91,6 +93,14 @@ static void tryBind(ServerSocketChannel ch, int... ports) throws IOException {
9193
}
9294
}
9395

96+
public void setSuccessResponse(Response successResponse) {
97+
this.successResponse = Objects.requireNonNull(successResponse);
98+
}
99+
100+
public void setErrorResponse(Response errorResponse) {
101+
this.errorResponse = Objects.requireNonNull(errorResponse);
102+
}
103+
94104
public URI getRedirectUri() {
95105
try {
96106
// use 127.0.0.1, not "localhost", see https://datatracker.ietf.org/doc/html/rfc8252#section-8.3
@@ -126,23 +136,24 @@ public String receive() throws IOException {
126136
throw new IOException("Unparseable Request", e);
127137
}
128138
if (!Path.of(path).equals(Path.of(requestUri.getPath()))) {
129-
HttpResponse.empty(HttpResponse.Status.NOT_FOUND).write(writer);
139+
Response.empty(Response.Status.NOT_FOUND).write(writer);
130140
throw new IOException("Requested invalid path " + requestUri);
131141
}
132142

133143
var params = URIUtil.parseQueryString(requestUri.getRawQuery());
134144
if (!csrfToken.equals(params.get("state"))) {
135-
HttpResponse.empty(HttpResponse.Status.BAD_REQUEST).write(writer);
145+
Response.empty(Response.Status.BAD_REQUEST).write(writer);
136146
throw new IOException("Missing or invalid state token");
137147
} else if (params.containsKey("error")) {
138-
var html = "<html><body>" + params.get("error") + "</body></html>";
139-
HttpResponse.html(HttpResponse.Status.OK, html).write(writer);
148+
// var html = "<html><body>" + params.get("error") + "</body></html>";
149+
// Response.html(Response.Status.OK, html).write(writer);
150+
errorResponse.write(writer); // TODO insert error code?
140151
throw new IOException("Authorization failed"); // TODO more specific exception containing the error code
141152
} else if (params.containsKey("code")) {
142153
successResponse.write(writer);
143154
return params.get("code");
144155
} else {
145-
HttpResponse.empty(HttpResponse.Status.BAD_REQUEST).write(writer);
156+
Response.empty(Response.Status.BAD_REQUEST).write(writer);
146157
throw new IOException("Missing authorization code");
147158
}
148159
}
@@ -159,16 +170,16 @@ public String receive() throws IOException {
159170
static URI parseRequestLine(String requestLine) throws InvalidRequestException {
160171
var words = requestLine.split(" ");
161172
if (words.length < 3) {
162-
throw new InvalidRequestException(HttpResponse.empty(HttpResponse.Status.BAD_REQUEST));
173+
throw new InvalidRequestException(Response.empty(Response.Status.BAD_REQUEST));
163174
}
164175
var method = words[0];
165176
if (!"GET".equals(method)) {
166-
throw new InvalidRequestException(HttpResponse.empty(HttpResponse.Status.METHOD_NOT_ALLOWED));
177+
throw new InvalidRequestException(Response.empty(Response.Status.METHOD_NOT_ALLOWED));
167178
}
168179
try {
169180
return new URI(words[1]);
170181
} catch (URISyntaxException e) {
171-
throw new InvalidRequestException(HttpResponse.empty(HttpResponse.Status.BAD_REQUEST));
182+
throw new InvalidRequestException(Response.empty(Response.Status.BAD_REQUEST));
172183
}
173184
}
174185

src/main/java/io/github/coffeelibs/tinyoauth2client/http/HttpResponse.java renamed to src/main/java/io/github/coffeelibs/tinyoauth2client/http/Response.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,20 @@
44
import java.io.Writer;
55
import java.net.URI;
66

7-
interface HttpResponse {
7+
public interface Response {
88

99
void write(Writer writer) throws IOException;
1010

11-
static HttpResponse empty(Status status) {
12-
return new HttpEmptyResponse(status);
11+
static Response empty(Status status) {
12+
return new EmptyResponse(status);
1313
}
1414

15-
static HttpResponse html(Status status, String body) {
16-
return new HttpHtmlResponse(status, body);
15+
static Response html(Status status, String body) {
16+
return new HtmlResponse(status, body);
1717
}
1818

19-
static HttpResponse redirect(URI target) {
20-
return new HttpRedirectResponse(Status.SEE_OTHER, target);
19+
static Response redirect(URI target) {
20+
return new RedirectResponse(Status.SEE_OTHER, target);
2121
}
2222

2323
enum Status {

0 commit comments

Comments
 (0)