Skip to content

Commit

Permalink
Merge branch 'release/0.2.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
overheadhunter committed Apr 28, 2022
2 parents 6d145ff + abcca77 commit 981cdb1
Show file tree
Hide file tree
Showing 18 changed files with 304 additions and 93 deletions.
3 changes: 0 additions & 3 deletions .idea/inspectionProfiles/Project_Default.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ support for [PKCE](https://datatracker.ietf.org/doc/html/rfc8252#section-8.1) an
## Usage

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

* Often you just need to authorize your client and nothing more. Most OAuth2 libraries try to do a lot more
* Nano-tiny-minuscule attack surface, since this doesn't contain any JOSE/JWT signature code, nor a fully-fleged web server
* Nano-tiny-minuscule attack surface, since this doesn't contain any JOSE/JWT signature code, nor a fully-fledged web server
* Focus is strictly on the authorization flow. Use any library for dealing with the tokens, you like.
* Modular jar, exposing only one single public API. No need to read docs, you can't do anything wrong.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>io.github.coffeelibs</groupId>
<artifactId>tiny-oauth2-client</artifactId>
<version>0.1.1</version>
<version>0.2.0</version>
<name>Tiny OAuth2 Client</name>
<description>Zero Dependency RFC 8252 Authorization Flow</description>
<inceptionYear>2022</inceptionYear>
Expand Down
64 changes: 62 additions & 2 deletions src/main/java/io/github/coffeelibs/tinyoauth2client/AuthFlow.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package io.github.coffeelibs.tinyoauth2client;

import io.github.coffeelibs.tinyoauth2client.http.RedirectTarget;
import io.github.coffeelibs.tinyoauth2client.http.Response;
import io.github.coffeelibs.tinyoauth2client.util.RandomUtil;
import org.jetbrains.annotations.Blocking;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.VisibleForTesting;

import java.io.IOException;
Expand Down Expand Up @@ -32,6 +34,9 @@ public class AuthFlow {
@VisibleForTesting
final PKCE pkce;

private Response successResponse;
private Response errorResponse;

private AuthFlow(String clientId) {
this.clientId = clientId;
this.pkce = new PKCE();
Expand All @@ -47,6 +52,54 @@ public static AuthFlow asClient(String clientId) {
return new AuthFlow(clientId);
}

/**
* HTML to display in the Resource Owner's user agent after successful authorization.
*
* @param html content served with {@code Content-Type: text/html; charset=UTF-8}
* @return this
*/
@Contract("_ -> this")
public AuthFlow withSuccessHtml(String html) {
this.successResponse = Response.html(Response.Status.OK, html);
return this;
}

/**
* Where to redirect the Resource Owner's user agent after successful authorization.
*
* @param target URI of page to show
* @return this
*/
@Contract("_ -> this")
public AuthFlow withSuccessRedirect(URI target) {
this.successResponse = Response.redirect(target);
return this;
}

/**
* HTML to display in the Resource Owner's user agent after failed authorization.
*
* @param html content served with {@code Content-Type: text/html; charset=UTF-8}
* @return this
*/
@Contract("_ -> this")
public AuthFlow withErrorHtml(String html) {
this.errorResponse = Response.html(Response.Status.OK, html);
return this;
}

/**
* Where to redirect the Resource Owner's user agent after failed authorization.
*
* @param target URI of page to show
* @return this
*/
@Contract("_ -> this")
public AuthFlow withErrorRedirect(URI target) {
this.errorResponse = Response.redirect(target);
return this;
}

/**
* Asks the given {@code browser} to browse the authorization URI. This method will block until the browser is
* <a href="https://datatracker.ietf.org/doc/html/rfc8252#section-4.1">redirected back to this application</a>.
Expand All @@ -58,6 +111,7 @@ public static AuthFlow asClient(String clientId) {
* @throws IOException In case of I/O errors during communication between browser and this application
* @see #authorize(URI, Consumer, Set, String, int...)
*/
@Blocking
public AuthFlowWithCode authorize(URI authEndpoint, Consumer<URI> browser, String... scopes) throws IOException {
return authorize(authEndpoint, browser, Set.of(scopes), "/" + RandomUtil.randomToken(16));
}
Expand All @@ -77,6 +131,12 @@ public AuthFlowWithCode authorize(URI authEndpoint, Consumer<URI> browser, Strin
@Blocking
public AuthFlowWithCode authorize(URI authEndpoint, Consumer<URI> browser, Set<String> scopes, String path, int... ports) throws IOException {
try (var redirectTarget = RedirectTarget.start(path, ports)) {
if (successResponse != null) {
redirectTarget.setSuccessResponse(successResponse);
}
if (errorResponse != null) {
redirectTarget.setErrorResponse(errorResponse);
}
var encodedRedirectUri = URLEncoder.encode(redirectTarget.getRedirectUri().toASCIIString(), StandardCharsets.US_ASCII);

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

@VisibleForTesting
AuthFlowWithCode(String encodedRedirectUri, String authorizationCode) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
import java.io.IOException;
import java.io.Writer;

class HttpEmptyResponse implements HttpResponse {
class EmptyResponse implements Response {

private final Status status;

public HttpEmptyResponse(HttpResponse.Status status) {
public EmptyResponse(Response.Status status) {
this.status = status;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
import java.io.Writer;
import java.nio.charset.StandardCharsets;

class HttpHtmlResponse implements HttpResponse {
class HtmlResponse implements Response {

private final Status status;
private final String body;

public HttpHtmlResponse(Status status, String body) {
public HtmlResponse(Status status, String body) {
this.status = status;
this.body = body;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package io.github.coffeelibs.tinyoauth2client.http;

public class InvalidRequestException extends Exception {
public final HttpResponse suggestedResponse;
public final Response suggestedResponse;

public InvalidRequestException(HttpResponse suggestedResponse) {
public InvalidRequestException(Response suggestedResponse) {
this.suggestedResponse = suggestedResponse;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
import java.io.Writer;
import java.net.URI;

class HttpRedirectResponse implements HttpResponse {
class RedirectResponse implements Response {

private final Status status;
private URI target;

public HttpRedirectResponse(Status status, URI target) {
public RedirectResponse(Status status, URI target) {
this.status = status;
this.target = target;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import java.nio.channels.ServerSocketChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.Objects;

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

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

private RedirectTarget(ServerSocketChannel serverChannel, String path) {
this.serverChannel = serverChannel;
Expand Down Expand Up @@ -91,6 +93,14 @@ static void tryBind(ServerSocketChannel ch, int... ports) throws IOException {
}
}

public void setSuccessResponse(Response successResponse) {
this.successResponse = Objects.requireNonNull(successResponse);
}

public void setErrorResponse(Response errorResponse) {
this.errorResponse = Objects.requireNonNull(errorResponse);
}

public URI getRedirectUri() {
try {
// use 127.0.0.1, not "localhost", see https://datatracker.ietf.org/doc/html/rfc8252#section-8.3
Expand Down Expand Up @@ -126,23 +136,24 @@ public String receive() throws IOException {
throw new IOException("Unparseable Request", e);
}
if (!Path.of(path).equals(Path.of(requestUri.getPath()))) {
HttpResponse.empty(HttpResponse.Status.NOT_FOUND).write(writer);
Response.empty(Response.Status.NOT_FOUND).write(writer);
throw new IOException("Requested invalid path " + requestUri);
}

var params = URIUtil.parseQueryString(requestUri.getRawQuery());
if (!csrfToken.equals(params.get("state"))) {
HttpResponse.empty(HttpResponse.Status.BAD_REQUEST).write(writer);
Response.empty(Response.Status.BAD_REQUEST).write(writer);
throw new IOException("Missing or invalid state token");
} else if (params.containsKey("error")) {
var html = "<html><body>" + params.get("error") + "</body></html>";
HttpResponse.html(HttpResponse.Status.OK, html).write(writer);
// var html = "<html><body>" + params.get("error") + "</body></html>";
// Response.html(Response.Status.OK, html).write(writer);
errorResponse.write(writer); // TODO insert error code?
throw new IOException("Authorization failed"); // TODO more specific exception containing the error code
} else if (params.containsKey("code")) {
successResponse.write(writer);
return params.get("code");
} else {
HttpResponse.empty(HttpResponse.Status.BAD_REQUEST).write(writer);
Response.empty(Response.Status.BAD_REQUEST).write(writer);
throw new IOException("Missing authorization code");
}
}
Expand All @@ -159,16 +170,16 @@ public String receive() throws IOException {
static URI parseRequestLine(String requestLine) throws InvalidRequestException {
var words = requestLine.split(" ");
if (words.length < 3) {
throw new InvalidRequestException(HttpResponse.empty(HttpResponse.Status.BAD_REQUEST));
throw new InvalidRequestException(Response.empty(Response.Status.BAD_REQUEST));
}
var method = words[0];
if (!"GET".equals(method)) {
throw new InvalidRequestException(HttpResponse.empty(HttpResponse.Status.METHOD_NOT_ALLOWED));
throw new InvalidRequestException(Response.empty(Response.Status.METHOD_NOT_ALLOWED));
}
try {
return new URI(words[1]);
} catch (URISyntaxException e) {
throw new InvalidRequestException(HttpResponse.empty(HttpResponse.Status.BAD_REQUEST));
throw new InvalidRequestException(Response.empty(Response.Status.BAD_REQUEST));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,20 @@
import java.io.Writer;
import java.net.URI;

interface HttpResponse {
public interface Response {

void write(Writer writer) throws IOException;

static HttpResponse empty(Status status) {
return new HttpEmptyResponse(status);
static Response empty(Status status) {
return new EmptyResponse(status);
}

static HttpResponse html(Status status, String body) {
return new HttpHtmlResponse(status, body);
static Response html(Status status, String body) {
return new HtmlResponse(status, body);
}

static HttpResponse redirect(URI target) {
return new HttpRedirectResponse(Status.SEE_OTHER, target);
static Response redirect(URI target) {
return new RedirectResponse(Status.SEE_OTHER, target);
}

enum Status {
Expand Down
Loading

0 comments on commit 981cdb1

Please sign in to comment.