Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions gradle/native-build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,7 @@ ext.nativeBuildLib = { buildDir, projectDir, name, os ->
"-framework", "OpenGLES",
"-framework", "StoreKit",
"-framework", "UserNotifications",
"-framework", "AuthenticationServices",
"-arch_only", arch,
"-syslibroot", sdkPath(platform),
"-L${sdkPath(platform)}/usr/lib",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2016, 2019 Gluon
* Copyright (c) 2016, 2026, Gluon
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
Expand Down Expand Up @@ -32,17 +32,37 @@
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.Optional;
import java.util.function.Consumer;

/**
* Launches the default browser of the platform as a separate application process. The browser
* will be opened with the provided URL.
* will be opened with the provided URL by means of {@link #launchExternalBrowser(String)}.
*
* <p><b>Example</b></p>
* <pre>
* {@code BrowserService.create().ifPresent(service -> {
* service.launchExternalBrowser("https://gluonhq.com/");
* });}</pre>
*
* <p>The service can also be used to perform secure user authentication against a web service
* (for instance an OAuth 2.0 / OpenID Connect provider), using an embedded browser, by means of
* {@link #launchWebAuthentication(String, String, Consumer)}.</p>
*
* <p><b>Example</b></p>
* <pre>
* {@code BrowserService.create().ifPresent(service -> {
* service.launchWebAuthentication(
* "https://my-auth-provider.com/authorize?response_type=token&redirect_uri=myapp://callback",
* "myapp",
* callbackUrl -> {
* if (callbackUrl != null) {
* System.out.println("Authenticated, callback: " + callbackUrl);
* } else {
* System.out.println("Authentication cancelled or failed");
* }
* });
* });}</pre>
*
* <p><b>Android Configuration</b>: none</p>
* <p><b>iOS Configuration</b>: none</p>
*
Expand All @@ -66,4 +86,46 @@ static Optional<BrowserService> create() {
* @throws java.net.URISyntaxException If it is not a valid URL string
*/
void launchExternalBrowser(String url) throws IOException, URISyntaxException;

/**
* Starts a web authentication session that lets the user authenticate against a web
* service, and delivers the redirect (callback) URL back to the app once the
* authentication flow completes.
*
* <p>On <b>iOS</b> this is implemented with a secure, dedicated native web view on top of the app.
* When the web service redirects to a URL that matches {@code callbackUrlScheme},
* the session is automatically dismissed and the full callback URL with the authorization code is
* passed to {@code callback}. The redirect is secure and never travels through the system URL dispatch.</p>
*
* <p>The {@code callbackUrlScheme} can be provided in two forms:</p>
* <ul>
* <li><b>A custom URL scheme</b> (without {@code ://}, e.g. {@code "myapp"}), with a
* redirect like {@code myapp://callback}. No {@code Info.plist} URL scheme registration is
* required, since the session intercepts the redirect on its own.
* </li>
* <li><b>A full HTTPS URL</b> (e.g. {@code "https://example.com/callback"}), with a
* verified Universal Link redirect. This requires iOS 17.4 or higher and an
* {@code apple-app-site-association} file hosted on the domain that associates it with the app.
* </li>
* </ul>
*
* <p>On <b>Android</b> and <b>Desktop</b> the default implementation simply opens the URL in the
* external browser (see {@link #launchExternalBrowser(String)}). On Android the redirect is
* caught by the system through an HTTPS or custom-scheme intent filter declared in the
* {@code AndroidManifest.xml}, and the resulting URL can be read with the
* {@code RuntimeArgsService}.</p>
*
* @param url the authentication URL to load, including the {@code redirect_uri} expected by the
* web service.
* @param callbackUrlScheme either a custom URL scheme (without {@code ://}, e.g. {@code "myapp"})
* or a full HTTPS URL (e.g. {@code "https://example.com/callback"}) that
* the web service uses for its redirect.
* @param callback a consumer that receives the full callback URL on success, or {@code null} if
* the user canceled the flow or an error occurred.
* @throws java.io.IOException If the URL can't be opened
* @throws java.net.URISyntaxException If it is not a valid URL string
* @since 4.0.25
*/
void launchWebAuthentication(String url, String callbackUrlScheme, Consumer<String> callback)
throws IOException, URISyntaxException;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2016, 2020 Gluon
* Copyright (c) 2016, 2026, Gluon
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
Expand Down Expand Up @@ -31,6 +31,8 @@
import com.gluonhq.attach.util.Util;

import java.io.IOException;
import java.net.URISyntaxException;
import java.util.function.Consumer;
import java.util.logging.Logger;

public class AndroidBrowserService implements BrowserService {
Expand Down Expand Up @@ -58,6 +60,12 @@ public void launchExternalBrowser(String url) throws IOException {
}
}

@Override
public void launchWebAuthentication(String url, String callbackUrlScheme, Consumer<String> callback)
throws IOException, URISyntaxException {
launchExternalBrowser(url);
}
Comment thread
jperedadnr marked this conversation as resolved.

// native
private native boolean launchURL(String url);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2021, Gluon
* Copyright (c) 2021, 2026, Gluon
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
Expand Down Expand Up @@ -31,8 +31,10 @@
import com.gluonhq.attach.util.Util;

import java.io.IOException;
import java.net.URISyntaxException;
import java.util.List;
import java.util.Locale;
import java.util.function.Consumer;
import java.util.logging.Logger;

public class DesktopBrowserService implements BrowserService {
Expand Down Expand Up @@ -63,4 +65,10 @@ public void launchExternalBrowser(String url) throws IOException {
throw new IOException("Error launching url " + url);
}
}

@Override
public void launchWebAuthentication(String url, String callbackUrlScheme, Consumer<String> callback)
throws IOException, URISyntaxException {
launchExternalBrowser(url);
}
Comment thread
jperedadnr marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2016, 2019, Gluon
* Copyright (c) 2016, 2026, Gluon
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
Expand Down Expand Up @@ -29,13 +29,21 @@


import com.gluonhq.attach.browser.BrowserService;
import javafx.application.Platform;

import java.io.IOException;
import java.util.function.Consumer;
import java.util.logging.Logger;

public class IOSBrowserService implements BrowserService {

private static final Logger LOG = Logger.getLogger(IOSBrowserService.class.getName());

private static Consumer<String> authCallback;

static {
System.loadLibrary("Browser");
initBrowser();
}

@Override
Expand All @@ -44,7 +52,33 @@ public void launchExternalBrowser(String url) throws IOException {
throw new IOException("Error launching url " + url);
}
}


@Override
public void launchWebAuthentication(String url, String callbackUrlScheme, Consumer<String> callback) throws IOException {
if (url == null || url.isEmpty()) {
throw new IOException("Authentication url cannot be null or empty");
}
if (callbackUrlScheme == null || callbackUrlScheme.isEmpty()) {
throw new IOException("Callback url scheme cannot be null or empty");
}
authCallback = callback;
startWebAuthentication(url, callbackUrlScheme);
}
Comment thread
jperedadnr marked this conversation as resolved.

// native
private native boolean launchURL(String url);

private static native void initBrowser();
private static native void startWebAuthentication(String url, String callbackUrlScheme);

// callback
public static void setAuthResult(String callbackUrl) {
final Consumer<String> callback = authCallback;
authCallback = null;
if (callback == null) {
LOG.warning("No callback registered for web authentication result");
return;
}
Platform.runLater(() -> callback.accept(callbackUrl));
}
Comment thread
jperedadnr marked this conversation as resolved.

}
4 changes: 2 additions & 2 deletions modules/browser/src/main/java/module-info.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2019, Gluon
* Copyright (c) 2019, 2026, Gluon
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
Expand All @@ -26,7 +26,7 @@
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
module com.gluonhq.attach.browser {

requires javafx.graphics;
requires com.gluonhq.attach.util;

exports com.gluonhq.attach.browser;
Expand Down
122 changes: 121 additions & 1 deletion modules/browser/src/main/native/ios/Browser.m
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2016, 2019 Gluon
* Copyright (c) 2016, 2026, Gluon
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
Expand All @@ -26,11 +26,33 @@
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#import <UIKit/UIKit.h>
#import <AuthenticationServices/AuthenticationServices.h>
#include "jni.h"
#include "AttachMacros.h"

JNIEnv *env;

static int BrowserInited = 0;
jclass mat_jBrowserServiceClass;
jmethodID mat_jBrowserService_setAuthResult = 0;
Comment thread
jperedadnr marked this conversation as resolved.

API_AVAILABLE(ios(12.0))
static ASWebAuthenticationSession *_authSession;

API_AVAILABLE(ios(13.0))
@interface AttachAuthContextProvider : NSObject <ASWebAuthenticationPresentationContextProviding>
@end

API_AVAILABLE(ios(13.0))
@implementation AttachAuthContextProvider
- (ASPresentationAnchor)presentationAnchorForWebAuthenticationSession:(ASWebAuthenticationSession *)session {
return [[UIApplication sharedApplication] keyWindow];
}
Comment thread
jperedadnr marked this conversation as resolved.
@end

API_AVAILABLE(ios(13.0))
static AttachAuthContextProvider *_authContextProvider;

JNIEXPORT jint JNICALL
JNI_OnLoad_Browser(JavaVM *vm, void *reserved)
{
Expand All @@ -45,6 +67,32 @@
#endif
}

JNIEXPORT void JNICALL Java_com_gluonhq_attach_browser_impl_IOSBrowserService_initBrowser
(JNIEnv *env, jclass jClass)
{
if (BrowserInited)
{
return;
}
BrowserInited = 1;

mat_jBrowserServiceClass = (*env)->NewGlobalRef(env, (*env)->FindClass(env, "com/gluonhq/attach/browser/impl/IOSBrowserService"));
mat_jBrowserService_setAuthResult = (*env)->GetStaticMethodID(env, mat_jBrowserServiceClass, "setAuthResult", "(Ljava/lang/String;)V");
}

void sendAuthResult(NSString *callbackUrl) {
jstring arg = NULL;
if (callbackUrl != nil) {
const char *callbackChars = [callbackUrl UTF8String];
arg = (*env)->NewStringUTF(env, callbackChars);
}
(*env)->CallStaticVoidMethod(env, mat_jBrowserServiceClass, mat_jBrowserService_setAuthResult, arg);
if (arg != NULL) {
(*env)->DeleteLocalRef(env, arg);
}
AttachLog(@"Finished sending web authentication result");
}
Comment thread
jperedadnr marked this conversation as resolved.

JNIEXPORT jboolean JNICALL Java_com_gluonhq_attach_browser_impl_IOSBrowserService_launchURL
(JNIEnv *env, jclass jClass, jstring jUrl)
{
Expand Down Expand Up @@ -79,3 +127,75 @@
}
}

JNIEXPORT void JNICALL Java_com_gluonhq_attach_browser_impl_IOSBrowserService_startWebAuthentication
(JNIEnv *env, jclass jClass, jstring jUrl, jstring jScheme)
{
const jchar *charsUrl = (*env)->GetStringChars(env, jUrl, NULL);
NSString *url = [NSString stringWithCharacters:(UniChar *)charsUrl length:(*env)->GetStringLength(env, jUrl)];
(*env)->ReleaseStringChars(env, jUrl, charsUrl);

const jchar *charsScheme = (*env)->GetStringChars(env, jScheme, NULL);
NSString *scheme = [NSString stringWithCharacters:(UniChar *)charsScheme length:(*env)->GetStringLength(env, jScheme)];
(*env)->ReleaseStringChars(env, jScheme, charsScheme);

NSURL *nsUrl = [NSURL URLWithString:url];

if (@available(iOS 12.0, *)) {
Comment thread
jperedadnr marked this conversation as resolved.
void (^completionHandler)(NSURL * _Nullable, NSError * _Nullable) =
^(NSURL * _Nullable callbackURL, NSError * _Nullable error) {
if (error != nil || callbackURL == nil) {
AttachLog(@"Web authentication finished without a callback url: %@", error);
sendAuthResult(nil);
} else {
AttachLog(@"Web authentication succeeded with callback url: %@", callbackURL);
sendAuthResult([callbackURL absoluteString]);
}
_authSession = nil;
};

BOOL isHttps = [[scheme lowercaseString] hasPrefix:@"https"];
if (isHttps) {
// Verified Universal Link (https://example.com/callback): requires iOS 17.4+ and an apple-app-site-association
// file associating the domain with the app.
if (@available(iOS 17.4, *)) {
NSURLComponents *components = [NSURLComponents componentsWithString:scheme];
NSString *host = components.host;
NSString *path = (components.path != nil && [components.path length] > 0) ? components.path : @"/";
if (host == nil || [host length] == 0) {
AttachLog(@"Invalid https callback, a host is required: %@", scheme);
sendAuthResult(nil);
return;
}
ASWebAuthenticationSessionCallback *httpsCallback =
[ASWebAuthenticationSessionCallback callbackWithHTTPSHost:host path:path];
_authSession = [[ASWebAuthenticationSession alloc] initWithURL:nsUrl
callback:httpsCallback completionHandler:completionHandler];
} else {
AttachLog(@"https callback URLs require iOS 17.4 or higher");
sendAuthResult(nil);
return;
}
} else {
// Custom scheme callback ("myapp")
_authSession = [[ASWebAuthenticationSession alloc] initWithURL:nsUrl
callbackURLScheme:scheme completionHandler:completionHandler];
}

if (@available(iOS 13.0, *)) {
_authContextProvider = [[AttachAuthContextProvider alloc] init];
_authSession.presentationContextProvider = _authContextProvider;
_authSession.prefersEphemeralWebBrowserSession = NO;
}

dispatch_async(dispatch_get_main_queue(), ^{
if (![_authSession start]) {
AttachLog(@"Failed to start the web authentication session");
sendAuthResult(nil);
_authSession = nil;
}
});
} else {
AttachLog(@"ASWebAuthenticationSession requires iOS 12.0 or higher");
sendAuthResult(nil);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[
{
"name" : "com.gluonhq.attach.browser.impl.IOSBrowserService",
"methods":[{"name":"setAuthResult","parameterTypes":["java.lang.String"] }]
}
]

Loading
Loading