2626
2727
2828import java .io .IOException ;
29+ import java .net .URI ;
30+ import java .net .URISyntaxException ;
2931import java .nio .charset .Charset ;
3032import java .util .List ;
3133
3234import android .annotation .TargetApi ;
3335import android .content .Context ;
3436import android .graphics .Bitmap ;
3537import android .net .Uri ;
38+ import android .os .Build ;
3639import android .os .Build .VERSION_CODES ;
3740import android .os .Handler ;
3841import android .os .Parcelable ;
4447import androidx .annotation .Nullable ;
4548import androidx .appcompat .app .AppCompatActivity ;
4649import androidx .fragment .app .FragmentManager ;
50+
4751import com .google .auto .value .AutoValue ;
52+
4853import de .cotech .hw .fido .internal .jsapi .U2fApiUtils ;
4954import de .cotech .hw .fido .internal .jsapi .U2fAuthenticateRequest ;
5055import de .cotech .hw .fido .internal .jsapi .U2fJsonParser ;
6065import de .cotech .hw .util .HwTimber ;
6166
6267
63- @ TargetApi (VERSION_CODES .LOLLIPOP )
68+ /**
69+ * If you are using a WebView for your login flow, you can use this WebViewFidoBridge
70+ * for extending the WebView's Javascript API with the official FIDO U2F APIs.
71+ * <p>
72+ * Currently supported:
73+ * - High level API of U2F v1.1, https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-javascript-api-v1.2-ps-20170411.html
74+ * <p>
75+ * Note: Currently only compatible and tested with Android SDK >= 19 due to evaluateJavascript() calls.
76+ */
77+ @ TargetApi (VERSION_CODES .KITKAT )
6478public class WebViewFidoBridge {
6579 private static final String FIDO_BRIDGE_INTERFACE = "fidobridgejava" ;
6680 private static final String ASSETS_BRIDGE_JS = "fidobridge.js" ;
6781
6882 private final Context context ;
6983 private final FragmentManager fragmentManager ;
7084 private final WebView webView ;
85+ private final FidoDialogOptions .Builder optionsBuilder ;
7186
7287 private String currentLoadedHost ;
7388 private boolean loadingNewPage ;
7489
75-
90+ @ SuppressWarnings ( "unused" ) // public API
7691 public static WebViewFidoBridge createInstanceForWebView (AppCompatActivity activity , WebView webView ) {
77- return createInstanceForWebView (activity .getApplicationContext (), activity .getSupportFragmentManager (), webView );
92+ return createInstanceForWebView (activity .getApplicationContext (), activity .getSupportFragmentManager (), webView , null );
7893 }
7994
80- // TODO should this be public API?
81- private static WebViewFidoBridge createInstanceForWebView (
82- Context context , FragmentManager fragmentManager , WebView webView ) {
95+ /**
96+ * Same as createInstanceForWebView, but allows to set FidoDialogOptions.
97+ * <p>
98+ * Note: Timeout and Title will be overwritten.
99+ */
100+ @ SuppressWarnings ("unused" ) // public API
101+ public static WebViewFidoBridge createInstanceForWebView (AppCompatActivity activity , WebView webView , FidoDialogOptions .Builder optionsBuilder ) {
102+ return createInstanceForWebView (activity .getApplicationContext (), activity .getSupportFragmentManager (), webView , optionsBuilder );
103+ }
104+
105+ public static WebViewFidoBridge createInstanceForWebView (Context context , FragmentManager fragmentManager , WebView webView ) {
106+ return createInstanceForWebView (context , fragmentManager , webView , null );
107+ }
108+
109+ @ SuppressWarnings ("WeakerAccess" ) // public API
110+ public static WebViewFidoBridge createInstanceForWebView (Context context , FragmentManager fragmentManager , WebView webView , FidoDialogOptions .Builder optionsBuilder ) {
83111 Context applicationContext = context .getApplicationContext ();
84112
85- WebViewFidoBridge webViewFidoBridge = new WebViewFidoBridge (applicationContext , fragmentManager , webView );
113+ WebViewFidoBridge webViewFidoBridge = new WebViewFidoBridge (applicationContext , fragmentManager , webView , optionsBuilder );
86114 webViewFidoBridge .addJavascriptInterfaceToWebView ();
87115
88116 return webViewFidoBridge ;
89117 }
90118
91-
92- private WebViewFidoBridge (Context context , FragmentManager fragmentManager , WebView webView ) {
119+ private WebViewFidoBridge (Context context , FragmentManager fragmentManager , WebView webView , FidoDialogOptions .Builder optionsBuilder ) {
93120 this .context = context ;
94121 this .fragmentManager = fragmentManager ;
95122 this .webView = webView ;
123+ this .optionsBuilder = optionsBuilder ;
96124 }
97125
98126 private void addJavascriptInterfaceToWebView () {
@@ -111,16 +139,26 @@ public void sign(String requestJson) {
111139
112140 // region delegate
113141
114- @ SuppressWarnings ("unused" ) // parity with WebViewClient.shouldInterceptRequest
142+ /**
143+ * Call this in your WebViewClient.shouldInterceptRequest(WebView view, WebResourceRequest request)
144+ */
145+ @ TargetApi (VERSION_CODES .LOLLIPOP )
146+ @ SuppressWarnings ("unused" )
147+ // parity with WebViewClient.shouldInterceptRequest(WebView view, WebResourceRequest request)
115148 public void delegateShouldInterceptRequest (WebView view , WebResourceRequest request ) {
116- HwTimber .d ("shouldInterceptRequest %s" , request .getUrl ());
149+ HwTimber .d ("shouldInterceptRequest(WebView view, WebResourceRequest request) %s" , request .getUrl ());
150+ injectOnInterceptRequest ();
151+ }
117152
118- if (loadingNewPage ) {
119- loadingNewPage = false ;
120- HwTimber .d ("Scheduling fido bridge injection!" );
121- Handler handler = new Handler (context .getMainLooper ());
122- handler .postAtFrontOfQueue (this ::injectJavascriptFidoBridge );
123- }
153+ /**
154+ * Call this in your WebViewClient.shouldInterceptRequest(WebView view, String url)
155+ */
156+ @ TargetApi (VERSION_CODES .KITKAT )
157+ @ SuppressWarnings ("unused" )
158+ // parity with WebViewClient.shouldInterceptRequest(WebView view, String url)
159+ public void delegateShouldInterceptRequest (WebView view , String url ) {
160+ HwTimber .d ("shouldInterceptRequest(WebView view, String url): %s" , url );
161+ injectOnInterceptRequest ();
124162 }
125163
126164 @ SuppressWarnings ("unused" ) // parity with WebViewClient.onPageStarted
@@ -142,6 +180,15 @@ public void delegateOnPageStarted(WebView view, String url, Bitmap favicon) {
142180 this .loadingNewPage = true ;
143181 }
144182
183+ private void injectOnInterceptRequest () {
184+ if (loadingNewPage ) {
185+ loadingNewPage = false ;
186+ HwTimber .d ("Scheduling fido bridge injection!" );
187+ Handler handler = new Handler (context .getMainLooper ());
188+ handler .postAtFrontOfQueue (this ::injectJavascriptFidoBridge );
189+ }
190+ }
191+
145192 private void injectJavascriptFidoBridge () {
146193 try {
147194 String jsContent = AndroidUtils .loadTextFromAssets (context , ASSETS_BRIDGE_JS , Charset .defaultCharset ());
@@ -179,11 +226,15 @@ private void handleRegisterRequest(String requestJson) {
179226 }
180227
181228 private void showRegisterFragment (RequestData requestData , String appId , String challenge ,
182- Long timeoutSeconds ) {
229+ Long timeoutSeconds ) {
183230 FidoRegisterRequest registerRequest = FidoRegisterRequest .create (
184231 appId , getCurrentFacetId (), challenge , requestData );
185- FidoDialogOptions fidoDialogOptions = getFidoDialogOptions (timeoutSeconds );
186- FidoDialogFragment fidoDialogFragment = FidoDialogFragment .newInstance (registerRequest , fidoDialogOptions );
232+
233+ FidoDialogOptions .Builder opsBuilder = optionsBuilder != null ? optionsBuilder : FidoDialogOptions .builder ();
234+ opsBuilder .setTimeoutSeconds (timeoutSeconds );
235+ opsBuilder .setTitle (context .getString (R .string .hwsecurity_title_default_register_app_id , getDisplayAppId (appId )));
236+
237+ FidoDialogFragment fidoDialogFragment = FidoDialogFragment .newInstance (registerRequest , opsBuilder .build ());
187238 fidoDialogFragment .setFidoRegisterCallback (fidoRegisterCallback );
188239 fidoDialogFragment .show (fragmentManager );
189240 }
@@ -199,15 +250,12 @@ public void onFidoRegisterResponse(@NonNull FidoRegisterResponse registerRespons
199250
200251 @ Override
201252 public void onFidoRegisterCancel (@ NonNull FidoRegisterRequest fidoRegisterRequest ) {
202- // Google's Authenticator does not return any error code when the user closes the activity
203- // but we do
204- HwTimber .d ("onRegisterCancel" );
253+ // Google's Authenticator does not return error codes when the user closes the activity, but we do
205254 handleError (fidoRegisterRequest .getCustomData (), ErrorCode .OTHER_ERROR );
206255 }
207256
208257 @ Override
209258 public void onFidoRegisterTimeout (@ NonNull FidoRegisterRequest fidoRegisterRequest ) {
210- HwTimber .d ("onRegisterTimeout" );
211259 handleError (fidoRegisterRequest .getCustomData (), ErrorCode .TIMEOUT );
212260 }
213261 };
@@ -244,16 +292,19 @@ private void showSignFragment(
244292 Long timeoutSeconds ) {
245293 FidoAuthenticateRequest authenticateRequest = FidoAuthenticateRequest .create (
246294 appId , getCurrentFacetId (), challenge , keyHandles , requestData );
247- FidoDialogOptions fidoDialogOptions = getFidoDialogOptions (timeoutSeconds );
248- FidoDialogFragment fidoDialogFragment = FidoDialogFragment .newInstance (authenticateRequest , fidoDialogOptions );
295+
296+ FidoDialogOptions .Builder opsBuilder = optionsBuilder != null ? optionsBuilder : FidoDialogOptions .builder ();
297+ opsBuilder .setTimeoutSeconds (timeoutSeconds );
298+ opsBuilder .setTitle (context .getString (R .string .hwsecurity_title_default_authenticate_app_id , getDisplayAppId (appId )));
299+
300+ FidoDialogFragment fidoDialogFragment = FidoDialogFragment .newInstance (authenticateRequest , opsBuilder .build ());
249301 fidoDialogFragment .setFidoAuthenticateCallback (fidoAuthenticateCallback );
250302 fidoDialogFragment .show (fragmentManager );
251303 }
252304
253305 private OnFidoAuthenticateCallback fidoAuthenticateCallback = new OnFidoAuthenticateCallback () {
254306 @ Override
255307 public void onFidoAuthenticateResponse (@ NonNull FidoAuthenticateResponse authenticateResponse ) {
256- HwTimber .d ("onAuthenticateResponse" );
257308 U2fResponse u2fResponse = U2fResponse .createAuthenticateResponse (
258309 authenticateResponse .<RequestData >getCustomData ().getRequestId (),
259310 authenticateResponse .getClientData (),
@@ -264,15 +315,12 @@ public void onFidoAuthenticateResponse(@NonNull FidoAuthenticateResponse authent
264315
265316 @ Override
266317 public void onFidoAuthenticateCancel (@ NonNull FidoAuthenticateRequest fidoAuthenticateRequest ) {
267- // Google's Authenticator does not return any error code when the user closes the activity
268- // but we do
269- HwTimber .d ("onAuthenticateCancel" );
318+ // Google's Authenticator does not return error codes when the user closes the activity, but we do
270319 handleError (fidoAuthenticateRequest .getCustomData (), ErrorCode .OTHER_ERROR );
271320 }
272321
273322 @ Override
274323 public void onFidoAuthenticateTimeout (@ NonNull FidoAuthenticateRequest fidoAuthenticateRequest ) {
275- HwTimber .d ("onAuthenticateTimeout" );
276324 handleError (fidoAuthenticateRequest .getCustomData (), ErrorCode .TIMEOUT );
277325 }
278326 };
@@ -285,6 +333,15 @@ private String getCurrentFacetId() {
285333 return "https://" + currentLoadedHost ;
286334 }
287335
336+ private String getDisplayAppId (String appId ) {
337+ try {
338+ URI appIdUri = new URI (appId );
339+ return appIdUri .getHost ();
340+ } catch (URISyntaxException e ) {
341+ throw new IllegalStateException ("Invalid URI used for appId" );
342+ }
343+ }
344+
288345 private void checkAppIdForFacet (String appId ) throws IOException {
289346 Uri appIdUri = Uri .parse (appId );
290347 String appIdHost = appIdUri .getHost ();
@@ -293,13 +350,6 @@ private void checkAppIdForFacet(String appId) throws IOException {
293350 }
294351 }
295352
296- private FidoDialogOptions getFidoDialogOptions (Long timeoutSeconds ) {
297- return FidoDialogOptions .builder ()
298- // .setTitle(getString(R.string.fido_authenticate, getDisplayAppId(u2fAuthenticateRequest.appId)))
299- .setTimeoutSeconds (timeoutSeconds )
300- .build ();
301- }
302-
303353 private void handleError (RequestData requestData , ErrorCode errorCode ) {
304354 U2fResponse u2fResponse = U2fResponse .createErrorResponse (
305355 requestData .getType (), requestData .getRequestId (), errorCode );
@@ -320,6 +370,7 @@ public static RequestData create(String type, Long requestId) {
320370 }
321371
322372 abstract String getType ();
373+
323374 @ Nullable
324375 abstract Long getRequestId ();
325376 }
0 commit comments