26
26
27
27
28
28
import java .io .IOException ;
29
+ import java .net .URI ;
30
+ import java .net .URISyntaxException ;
29
31
import java .nio .charset .Charset ;
30
32
import java .util .List ;
31
33
32
34
import android .annotation .TargetApi ;
33
35
import android .content .Context ;
34
36
import android .graphics .Bitmap ;
35
37
import android .net .Uri ;
38
+ import android .os .Build ;
36
39
import android .os .Build .VERSION_CODES ;
37
40
import android .os .Handler ;
38
41
import android .os .Parcelable ;
44
47
import androidx .annotation .Nullable ;
45
48
import androidx .appcompat .app .AppCompatActivity ;
46
49
import androidx .fragment .app .FragmentManager ;
50
+
47
51
import com .google .auto .value .AutoValue ;
52
+
48
53
import de .cotech .hw .fido .internal .jsapi .U2fApiUtils ;
49
54
import de .cotech .hw .fido .internal .jsapi .U2fAuthenticateRequest ;
50
55
import de .cotech .hw .fido .internal .jsapi .U2fJsonParser ;
60
65
import de .cotech .hw .util .HwTimber ;
61
66
62
67
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 )
64
78
public class WebViewFidoBridge {
65
79
private static final String FIDO_BRIDGE_INTERFACE = "fidobridgejava" ;
66
80
private static final String ASSETS_BRIDGE_JS = "fidobridge.js" ;
67
81
68
82
private final Context context ;
69
83
private final FragmentManager fragmentManager ;
70
84
private final WebView webView ;
85
+ private final FidoDialogOptions .Builder optionsBuilder ;
71
86
72
87
private String currentLoadedHost ;
73
88
private boolean loadingNewPage ;
74
89
75
-
90
+ @ SuppressWarnings ( "unused" ) // public API
76
91
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 );
78
93
}
79
94
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 ) {
83
111
Context applicationContext = context .getApplicationContext ();
84
112
85
- WebViewFidoBridge webViewFidoBridge = new WebViewFidoBridge (applicationContext , fragmentManager , webView );
113
+ WebViewFidoBridge webViewFidoBridge = new WebViewFidoBridge (applicationContext , fragmentManager , webView , optionsBuilder );
86
114
webViewFidoBridge .addJavascriptInterfaceToWebView ();
87
115
88
116
return webViewFidoBridge ;
89
117
}
90
118
91
-
92
- private WebViewFidoBridge (Context context , FragmentManager fragmentManager , WebView webView ) {
119
+ private WebViewFidoBridge (Context context , FragmentManager fragmentManager , WebView webView , FidoDialogOptions .Builder optionsBuilder ) {
93
120
this .context = context ;
94
121
this .fragmentManager = fragmentManager ;
95
122
this .webView = webView ;
123
+ this .optionsBuilder = optionsBuilder ;
96
124
}
97
125
98
126
private void addJavascriptInterfaceToWebView () {
@@ -111,16 +139,26 @@ public void sign(String requestJson) {
111
139
112
140
// region delegate
113
141
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)
115
148
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
+ }
117
152
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 ();
124
162
}
125
163
126
164
@ SuppressWarnings ("unused" ) // parity with WebViewClient.onPageStarted
@@ -142,6 +180,15 @@ public void delegateOnPageStarted(WebView view, String url, Bitmap favicon) {
142
180
this .loadingNewPage = true ;
143
181
}
144
182
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
+
145
192
private void injectJavascriptFidoBridge () {
146
193
try {
147
194
String jsContent = AndroidUtils .loadTextFromAssets (context , ASSETS_BRIDGE_JS , Charset .defaultCharset ());
@@ -179,11 +226,15 @@ private void handleRegisterRequest(String requestJson) {
179
226
}
180
227
181
228
private void showRegisterFragment (RequestData requestData , String appId , String challenge ,
182
- Long timeoutSeconds ) {
229
+ Long timeoutSeconds ) {
183
230
FidoRegisterRequest registerRequest = FidoRegisterRequest .create (
184
231
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 ());
187
238
fidoDialogFragment .setFidoRegisterCallback (fidoRegisterCallback );
188
239
fidoDialogFragment .show (fragmentManager );
189
240
}
@@ -199,15 +250,12 @@ public void onFidoRegisterResponse(@NonNull FidoRegisterResponse registerRespons
199
250
200
251
@ Override
201
252
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
205
254
handleError (fidoRegisterRequest .getCustomData (), ErrorCode .OTHER_ERROR );
206
255
}
207
256
208
257
@ Override
209
258
public void onFidoRegisterTimeout (@ NonNull FidoRegisterRequest fidoRegisterRequest ) {
210
- HwTimber .d ("onRegisterTimeout" );
211
259
handleError (fidoRegisterRequest .getCustomData (), ErrorCode .TIMEOUT );
212
260
}
213
261
};
@@ -244,16 +292,19 @@ private void showSignFragment(
244
292
Long timeoutSeconds ) {
245
293
FidoAuthenticateRequest authenticateRequest = FidoAuthenticateRequest .create (
246
294
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 ());
249
301
fidoDialogFragment .setFidoAuthenticateCallback (fidoAuthenticateCallback );
250
302
fidoDialogFragment .show (fragmentManager );
251
303
}
252
304
253
305
private OnFidoAuthenticateCallback fidoAuthenticateCallback = new OnFidoAuthenticateCallback () {
254
306
@ Override
255
307
public void onFidoAuthenticateResponse (@ NonNull FidoAuthenticateResponse authenticateResponse ) {
256
- HwTimber .d ("onAuthenticateResponse" );
257
308
U2fResponse u2fResponse = U2fResponse .createAuthenticateResponse (
258
309
authenticateResponse .<RequestData >getCustomData ().getRequestId (),
259
310
authenticateResponse .getClientData (),
@@ -264,15 +315,12 @@ public void onFidoAuthenticateResponse(@NonNull FidoAuthenticateResponse authent
264
315
265
316
@ Override
266
317
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
270
319
handleError (fidoAuthenticateRequest .getCustomData (), ErrorCode .OTHER_ERROR );
271
320
}
272
321
273
322
@ Override
274
323
public void onFidoAuthenticateTimeout (@ NonNull FidoAuthenticateRequest fidoAuthenticateRequest ) {
275
- HwTimber .d ("onAuthenticateTimeout" );
276
324
handleError (fidoAuthenticateRequest .getCustomData (), ErrorCode .TIMEOUT );
277
325
}
278
326
};
@@ -285,6 +333,15 @@ private String getCurrentFacetId() {
285
333
return "https://" + currentLoadedHost ;
286
334
}
287
335
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
+
288
345
private void checkAppIdForFacet (String appId ) throws IOException {
289
346
Uri appIdUri = Uri .parse (appId );
290
347
String appIdHost = appIdUri .getHost ();
@@ -293,13 +350,6 @@ private void checkAppIdForFacet(String appId) throws IOException {
293
350
}
294
351
}
295
352
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
-
303
353
private void handleError (RequestData requestData , ErrorCode errorCode ) {
304
354
U2fResponse u2fResponse = U2fResponse .createErrorResponse (
305
355
requestData .getType (), requestData .getRequestId (), errorCode );
@@ -320,6 +370,7 @@ public static RequestData create(String type, Long requestId) {
320
370
}
321
371
322
372
abstract String getType ();
373
+
323
374
@ Nullable
324
375
abstract Long getRequestId ();
325
376
}
0 commit comments