-
Notifications
You must be signed in to change notification settings - Fork 9
/
Authenticator.java
507 lines (464 loc) · 19.2 KB
/
Authenticator.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
package net.hycrafthd.minecraft_authenticator.login;
import java.net.URL;
import java.net.URLConnection;
import java.util.Optional;
import net.hycrafthd.minecraft_authenticator.Constants;
import net.hycrafthd.minecraft_authenticator.microsoft.AzureApplication;
import net.hycrafthd.minecraft_authenticator.microsoft.MicrosoftAuthentication;
import net.hycrafthd.minecraft_authenticator.microsoft.MicrosoftAuthenticationFile;
import net.hycrafthd.minecraft_authenticator.microsoft.MicrosoftLoginResponse;
import net.hycrafthd.minecraft_authenticator.microsoft.service.MicrosoftService;
import net.hycrafthd.minecraft_authenticator.util.ConnectionUtil.TimeoutValues;
/**
* <p>
* Main class to authenticate a user with minecraft services. <br>
* First some information about microsoft accounts:
* </p>
* <p>
* Microsoft accounts use oAuth. That is why you need to use a browser to login with your microsoft account. You will
* only get an authorization code which will be used for authentication. After the first login the
* {@link MicrosoftAuthenticationFile} contains a refresh token which will be used for refreshing the authentication.
* </p>
* <p>
* Even though the {@link AuthenticationFile} does not contain the real login data it contains sensitive data which can
* be used to authenticate to certain services including your minecraft account. This file should therefore be kept
* <b>secret</b> and should not be shared with others.
* </p>
* <p>
* Here is an example to on how to login into a microsoft account: <br>
* <br>
* This description only covers the basic oAuth for microsoft with the minecraft launcher id. If you want to customize
* the login you should create a custom azure application and use the custom azure authentication method
* {@link Builder#customAzureApplication(String, String)}. <br>
* To log into a microsoft account you need the authorization code that you get after you log into your microsoft
* account. First you need to open the {@link #microsoftLogin()} url in a browser (or in an integrated browser like
* javafx) and let the user login. After that you will be redirected to a page where the authorization code is the code
* url parameter. The url looks like this: https://login.live.com/oauth20_desktop.srf?code=M.XYZTHISISMYCODE
* </p>
*
* <pre>
* // Build authenticator
* final Authenticator authenticator = Authenticator.ofMicrosoft(authorizationCode).shouldAuthenticate().build();
* try {
* // Run authentication
* authenticator.run();
* } catch (final AuthenticationException ex) {
* // Always check if result file is present when an exception is thrown
* final AuthenticationFile file = authenticator.getResultFile();
* if (file != null) {
* // Save authentication file
* file.writeCompressed(outputStream);
* }
*
* // Show user error or rethrow
* throw ex;
* }
*
* // Save authentication file
* final AuthenticationFile file = authenticator.getResultFile();
* file.writeCompressed(outputStream);
*
* // Get user
* final Optional user = authenticator.getUser();
* </pre>
* <p>
* You get an {@link AuthenticationFile} and an {@link Optional} with a user if there was no error and you called
* {@link Builder#shouldAuthenticate()} before. After that the {@link AuthenticationFile} should be stored somewhere for
* reuse. If an error occurred save the {@link AuthenticationFile} as well if it is not null.
* </p>
* <p>
* To refresh a session you can use the saved {@link AuthenticationFile} and login like that:
* </p>
*
* <pre>
* // Build authenticator
* final Authenticator authenticator = Authenticator.of(authFile).shouldAuthenticate().build();
* try {
* // Run authentication
* authenticator.run();
* } catch (final AuthenticationException ex) {
* // Always check if result file is present when an exception is thrown
* final AuthenticationFile file = authenticator.getResultFile();
* if (file != null) {
* // Save authentication file
* file.writeCompressed(outputStream);
* }
*
* // Show user error or rethrow
* throw ex;
* }
*
* // Save authentication file
* final AuthenticationFile file = authenticator.getResultFile();
* file.writeCompressed(outputStream);
*
* // Get user
* final Optional user = authenticator.getUser();
* </pre>
* <p>
* After that save the returned {@link AuthenticationFile} again. The session should stay for a relative long time, but
* will be destroyed by certain events e.g. other client token, logout of all sessions, etc. The error message will tell
* you why the user cannot be authenticated. If a token is not valid anymore the user must relogin.
* </p>
* <p>
* If you need xbox profile settings call {@link Builder#shouldRetrieveXBoxProfile()} and
* {@link Builder#shouldAuthenticate()} when building the {@link Authenticator}. If the login was successful an
* {@link XBoxProfile} can be retrieved from {@link #getXBoxProfile()}.
* </p>
*/
public class Authenticator {
/**
* Creates an {@link Authenticator} of a {@link AuthenticationFile}.
*
* @see Authenticator
* @param file The {@link AuthenticationFile}
* @return A {@link Builder} to configure the authenticator
*/
public static Builder of(AuthenticationFile file) {
return new Builder(timeoutValues -> file);
}
/**
* Creates a microsoft {@link Authenticator} with a microsoft authorization code. See examples in the class javadoc.
*
* @see Authenticator
* @param authorizationCode Microsoft authorization code of the redirect url
* @return A {@link Builder} to configure the authenticator
*/
public static Builder ofMicrosoft(String authorizationCode) {
return new Builder((customAzureApplication, timeoutValues) -> MicrosoftAuthentication.createAuthenticationFile(customAzureApplication, authorizationCode, timeoutValues));
}
/**
* Returns the minecraft launcher oAuth login url for microsoft accounts. After the browser login the authorization code
* can be extracted from the redirect url.
*
* @see Authenticator
* @see Authenticator#microsoftLoginRedirect()
* @return oAuth microsoft login url
*/
public static URL microsoftLogin() {
return MicrosoftService.oAuthLoginUrl();
}
/**
* Return the minecraft launcher oAuth redirect url. This returns the start of the redirect url and should be used to
* match the redirect url and then to extract the authorization code.
*
* @see Authenticator
* @return oAuth microsoft redirect url
*/
public static String microsoftLoginRedirect() {
return Constants.MICROSOFT_OAUTH_REDIRECT_URL;
}
/**
* Returns the oAuth login url for your custom azure application. You need to extract the authorization code of the
* redirect url (depends on how you setup your azure application). If you don't want to use a custom azure application
* look at {@link Authenticator#microsoftLogin()}.
*
* @see Authenticator
* @param clientId The azure client id
* @param redirectUrl The configured redirect url
* @return oAuth microsoft login url
*/
public static URL microsoftLogin(String clientId, String redirectUrl) {
return MicrosoftService.oAuthLoginUrl(clientId, redirectUrl);
}
/**
* Internal builder class
*/
public static class Builder {
private final AuthenticationFileFunctionWithCustomAzureApplication fileFunction;
private boolean authenticate;
private boolean retrieveXBoxProfile;
private Optional<AzureApplication> customAzureApplication;
private int serviceConnectTimeout;
private int serviceReadTimeout;
/**
* Accepts a {@link AuthenticationFileFunction} which is just a normal supplier for an {@link AuthenticationFile} which
* can throw an {@link AuthenticationException}
*
* @param fileSupplier Supplier that returns {@link AuthenticationFile} for authentication
*/
protected Builder(AuthenticationFileFunction fileSupplier) {
this((customAzureApplication, timeoutValues) -> fileSupplier.get(timeoutValues));
}
/**
* Accepts a {@link AuthenticationFileFunctionWithCustomAzureApplication} which supplies the custom azure application
* values and returns a {@link AuthenticationFile} which can throw an {@link AuthenticationException}
*
* @param fileFunction Function that returns {@link AuthenticationFile} for authentication
*/
protected Builder(AuthenticationFileFunctionWithCustomAzureApplication fileFunction) {
this.fileFunction = fileFunction;
authenticate = false;
customAzureApplication = Optional.empty();
serviceConnectTimeout = 15000;
serviceReadTimeout = 15000;
}
/**
* Call this if you want to get a {@link User} object and authenticate to minecraft services
*
* @return This builder
*/
public Builder shouldAuthenticate() {
authenticate = true;
return this;
}
/**
* Call this if you want to get a {@link XBoxProfile} object. Only available for microsoft accounts. Only has an effect
* if {@link #shouldAuthenticate()} is called.
*
* @return This builder
*/
public Builder shouldRetrieveXBoxProfile() {
retrieveXBoxProfile = true;
return this;
}
/**
* Call this if you have a custom azure application that will handle the oauth for microsoft accounts
*
* @param clientId The azure client id
* @param redirectUrl The redirect url
* @return This builder
*/
public Builder customAzureApplication(String clientId, String redirectUrl) {
customAzureApplication = Optional.of(new AzureApplication(clientId, redirectUrl));
return this;
}
/**
* Call this if you have a custom azure application that will handle the oauth for microsoft accounts
*
* @param clientId The azure client id
* @param redirectUrl The redirect url
* @param clientSecret The client secret
* @return This builder
*/
public Builder customAzureApplication(String clientId, String redirectUrl, String clientSecret) {
customAzureApplication = Optional.of(new AzureApplication(clientId, redirectUrl, clientSecret));
return this;
}
/**
* Configure the connect timeout of a service request. This timeout configures
* {@link URLConnection#setConnectTimeout(int)} to the passed value for each service request
*
* @param timeout Timeout in milliseconds
* @return This builder
*/
public Builder serviceConnectTimeout(int timeout) {
serviceConnectTimeout = timeout;
return this;
}
/**
* Configure the read timeout of a service request. This timeout configures {@link URLConnection#setReadTimeout(int)} to
* the passed value for each service request
*
* @param timeout Timeout in milliseconds
* @return This builder
*/
public Builder serviceReadTimeout(int timeout) {
serviceReadTimeout = timeout;
return this;
}
/**
* Creates a new {@link Authenticator} with this configuration. To run the authenticator call
* {@link Authenticator#run()}.
*
* @return Build Authenticator object
*/
public Authenticator build() {
return new Authenticator(fileFunction, authenticate, retrieveXBoxProfile, customAzureApplication, new TimeoutValues(serviceConnectTimeout, serviceReadTimeout));
}
}
private final AuthenticationFileFunctionWithCustomAzureApplication fileFunction;
private final boolean authenticate;
private final boolean retrieveXBoxProfile;
private final Optional<AzureApplication> customAzureApplication;
private final TimeoutValues timeoutValues;
private boolean hasRun;
private AuthenticationFile resultFile;
private Optional<User> user;
private Optional<XBoxProfile> xBoxProfile;
/**
* Internal constructor to setup the authenticator state. To execute the authentication run the {@link #run()} method.
*
* @param fileFunction Function that returns {@link AuthenticationFile} for authentication
* @param authenticate Should authenticate to get a {@link User} as a result
* @param retrieveXBoxProfile Should retrieve an {@link XBoxProfile} object
* @param customAzureApplication Optional value to pass custom azure application values
* @param timeoutValues Timeout values for a service connection
*/
protected Authenticator(AuthenticationFileFunctionWithCustomAzureApplication fileFunction, boolean authenticate, boolean retrieveXBoxProfile, Optional<AzureApplication> customAzureApplication, TimeoutValues timeoutValues) {
this.fileFunction = fileFunction;
this.authenticate = authenticate;
this.retrieveXBoxProfile = retrieveXBoxProfile;
this.customAzureApplication = customAzureApplication;
this.timeoutValues = timeoutValues;
user = Optional.empty();
xBoxProfile = Optional.empty();
}
/**
* This runs the selected authentication tasks. If {@link Builder#shouldAuthenticate()} is not enabled it will only
* resolve the {@link AuthenticationFile}. This call is blocking and can take some time if the services take a long
* respond time. The default timeout time is 15 seconds per service request. Change the timeout for the services with
* {@link Builder#serviceConnectTimeout(int)} and {@link Builder#serviceReadTimeout(int)}.
* <p>
* Please always save the {@link #getResultFile()} if it is not null, even when {@link AuthenticationException} is
* thrown. This is important because when a service after the initial authentication fails the oAuth service still
* requires the updated tokens.
* </p>
* <p>
* This method can only be called once per {@link Authenticator} object.
* </p>
*
* @throws AuthenticationException Throws exception if login was not successful
*/
public void run() throws AuthenticationException {
run(LoginStateCallback.NOOP);
}
/**
* This runs the selected authentication tasks. If {@link Builder#shouldAuthenticate()} is not enabled it will only
* resolve the {@link AuthenticationFile}. This call is blocking and can take some time if the services take a long
* respond time. The default timeout time is 15 seconds per service request. Change the timeout for the services with
* {@link Builder#serviceConnectTimeout(int)} and {@link Builder#serviceReadTimeout(int)}.
* <p>
* Please always save the {@link #getResultFile()} if it is not null, even when {@link AuthenticationException} is
* thrown. This is important because when a service after the initial authentication fails the oAuth service still
* requires the updated tokens.
* </p>
* <p>
* This method can only be called once per {@link Authenticator} object.
* </p>
*
* @param callback Login state callback for information messages. Call is on thread and should not block too long
* @throws AuthenticationException Throws exception if login was not successful
*/
public void run(LoginStateCallback callback) throws AuthenticationException {
if (hasRun) {
throw new IllegalStateException("Cannot run the authentication multiple times");
}
hasRun = true;
// Resolve the initial file
callback.call(LoginState.INITAL_FILE);
resultFile = fileFunction.get(customAzureApplication, timeoutValues);
// Authentication
if (authenticate) {
// Microsoft authentication
if (resultFile instanceof final MicrosoftAuthenticationFile microsoftFile) {
final MicrosoftLoginResponse response = MicrosoftAuthentication.authenticate(customAzureApplication, retrieveXBoxProfile, microsoftFile, timeoutValues, callback);
// Set new result file
if (response.hasRefreshToken()) {
resultFile = new MicrosoftAuthenticationFile(microsoftFile.getClientId(), response.getRefreshToken().get());
}
// Throw exceptions
if (response.hasException()) {
throw response.getException().get();
}
// Validate authentication response
if (!response.hasUser()) {
throw new AuthenticationException("After login there should be a user");
}
user = response.getUser();
// Validate xbox profile response
if (retrieveXBoxProfile && !response.hasXBoxProfile()) {
throw new AuthenticationException("XBox profile was requested but is not there");
}
if (response.hasXBoxProfile()) {
xBoxProfile = response.getXBoxProfile();
}
} else {
throw new AuthenticationException(resultFile + " is not a microsoft authentication file");
}
}
}
/**
* Returns the updated {@link AuthenticationFile} if authentication was requested. Else returns the initial object. If
* the initial authentication fails this value might be <strong>null</strong>.
* <p>
* Can only be called after {@link #run()} was called.
* </p>
*
* @return {@link AuthenticationFile} that should be used for the next authentication or null
*/
public AuthenticationFile getResultFile() {
if (!hasRun) {
throw new IllegalStateException("This method can only be called after the authentication was run");
}
return resultFile;
}
/**
* Returns the user if authentication was requested and no errors occurred.
* <p>
* Can only be called after {@link #run()} was called.
* </p>
*
* @return Minecraft User. Cannot be empty if authentication was requested and no {@link AuthenticationException} was
* raised
*/
public Optional<User> getUser() {
if (!hasRun) {
throw new IllegalStateException("This method can only be called after the authentication was run");
}
return user;
}
/**
* Returns the XBox profile if authentication and the xbox profile was requested and no errors occurred.
* <p>
* Can only be called after {@link #run()} was called.
* </p>
*
* @return XBoxProfile. Cannot be empty if authentication and the xbox profile was requested and no
* {@link AuthenticationException} was raised
*/
public Optional<XBoxProfile> getXBoxProfile() {
if (!hasRun) {
throw new IllegalStateException("This method can only be called after the authentication was run");
}
return xBoxProfile;
}
/**
* Supplier that returns an {@link AuthenticationFile} and can trow an {@link AuthenticationException}
*/
@FunctionalInterface
protected interface AuthenticationFileFunction {
/**
* Returns the {@link AuthenticationFile}
*
* @param timeoutValues Timeout values for a service connection
* @return {@link AuthenticationFile}
* @throws AuthenticationException Throws if authentication file is created with an online service with authentication
*/
AuthenticationFile get(TimeoutValues timeoutValues) throws AuthenticationException;
}
/**
* Function that returns an {@link AuthenticationFile} and can trow an {@link AuthenticationException} and supplied the
* custom azure application parameters
*/
@FunctionalInterface
protected interface AuthenticationFileFunctionWithCustomAzureApplication {
/**
* Returns the {@link AuthenticationFile}
*
* @param customAzureApplication Custom azure application values that is needed to handle the oauth for microsoft
* accounts
* @param timeoutValues Timeout values for a service connection
* @return {@link AuthenticationFile}
* @throws AuthenticationException Throws if authentication file is created with an online service with authentication
*/
AuthenticationFile get(Optional<AzureApplication> customAzureApplication, TimeoutValues timeoutValues) throws AuthenticationException;
}
/**
* Functions that takes a {@link LoginState} as parameter. Can be used to display the current login state
*/
@FunctionalInterface
public interface LoginStateCallback {
/**
* Default callback
*/
static LoginStateCallback NOOP = state -> {
};
/**
* Consumes a {@link LoginState}
*
* @param state Current Login State
*/
void call(LoginState state);
}
}