I am using Azure AD to carry out the users authentication of a spring boot app.
I need to generate the token for some reasons from spring boot code and return it. So far this is what I have achieve.
1 Azure Ad config.
I have config a app within azure Ad and I have register One user and one group.
within the app i also have create a secret
Now in my spring boot app i have added the JWT filter and some config (I am not going to explain the full config because it will take a while)
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
#Profile("AzureAdSecurized")
public class AzureSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private RestAuthenticationExceptionHandler restAuthenticationExceptionHandler;
#Autowired
private AADAppRoleStatelessAuthenticationFilter aadAuthenticationFilter;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().sessionManagement().sessionCreationPolicy( SessionCreationPolicy.STATELESS );
http.headers().frameOptions().disable();
http.addFilterAfter(aadAuthenticationFilter, UsernamePasswordAuthenticationFilter.class );
http.addFilterBefore( new CorsFilter(), ChannelProcessingFilter.class );
http.exceptionHandling().authenticationEntryPoint( restAuthenticationExceptionHandler );
//Configuracion Endpoints
http.authorizeRequests().antMatchers( "/auth/login**" ).permitAll()
.antMatchers( "/v2/api-docs", "/configuration/**", "/swagger*/**", "/webjars/**" ).permitAll()
.antMatchers( "/actuator/**" ).permitAll().anyRequest().authenticated();
}
I also have added the following properties:
azure:
activedirectory:
tenant-id: 7XXXXX
client-id: 5XXXXX
session-stateless: true
spring:
security:
oauth2:
client:
registration:
azure:
client-id: 5XXX
client-secret: dXXXX
if I go to the following URL of microsoft:
https://login.microsoftonline.com//oauth2/authorize?client_id=&response_type=id_token&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Flogin&nonce=7362CAEA-9CA5–4B43–9BA3–34D7C303EBA7
I get a perfect Token in a redirection like: http://localhost:8080/login#id_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IllNRUxIVDBndmIwbXhvU0RvWWZvbWp
with this token I perfectly pass the aadAuthenticationFilter of spring boot security.
The point is that i have to generate this token from the app.
In my spring boot app I have algo add:
I have follow a tutorial but i dont no remeber url
#RestController
#RequestMapping(LoginPaths.AUTH)
#Profile("AzureAdSecurized")
public class AADLoginController {
private static final Logger LOG = LoggerFactory.getLogger( AADLoginController.class );
#RequestMapping(value = LoginPaths.LOGIN, method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public String generateTokenAzureAD() throws MalformedURLException, ExecutionException, InterruptedException {
ExecutorService service = Executors.newFixedThreadPool( 1 );
AuthenticationContext context = new AuthenticationContext(
"https://login.microsoftonline.com/<My-tenant>/oauth2/v2.0/authorize", false,
service );
Future<AuthenticationResult> future = context
.acquireToken( "https://graph.microsoft.com",
"<app-id>", "<username>",
"<pass>", null );
AuthenticationResult result = future.get();
LOG.info( "Access Token - " + result.getAccessToken() );
LOG.info( "Refresh Token - " + result.getRefreshToken() );
LOG.info( "ID Token - " + result.getIdToken() );
return "Bearer " + result.getAccessToken();
}
}
by passing username and password with classes of library "com.microsoft.aad.adal4j" I am trying to generate I token.
The token I receive is the following:
As you can see the token has information related with my user and so on and it seems to be correct, but the signature is not valid and if a send a request to my app with such token. obviously it gives me and error for invalid signature.
What I am doing wrong?
Thank you very much
Your token is correct. It is just a specific token for graph api. You will see a nonce in Jwt.Header. This means you need special processing. Normal processing will fail.
Update:
Access tokens are opaque blobs of text that are for the resource only.
If you're a client getting a token for Graph, assume that it's an
encrypted string that you should never look at - sometimes it will be.
We use a special token format for Graph that they know how to validate
- you shouldn't be looking at access tokens if they're not for you.
Reference:
https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/609#issuecomment-529537264
I am usisng the dependency instead:
<dependency>
<groupId>com.microsoft.azure</groupId>
<artifactId>msal4j</artifactId>
</dependency>
And generating tokens this way:
public TokenDTO generateTokenAzureAD(CredentialsDTO credentialsDTO) {
LoginValidator.validateLoginRequest( credentialsDTO );
PublicClientApplication app;
String AUTHORITY = authorityUrl + addTenantId;
try {
app = PublicClientApplication.builder( addAppId ).authority( AUTHORITY ).build();
} catch (MalformedURLException e) {
throw new MyException( ErrorCodes.ERROR_ADD_AUTHORITY_URL_NOT_VALID );
}
Set<String> scopes = Collections.singleton( addAppId + addAppScope );
UserNamePasswordParameters parameters = UserNamePasswordParameters
.builder( scopes, credentialsDTO.getUsername(), credentialsDTO.getPassword().toCharArray() ).build();
Future<IAuthenticationResult> result = app.acquireToken( parameters );
IAuthenticationResult auth;
try {
auth = result.get();
} catch (InterruptedException | ExecutionException e) {
throw new MyException( ErrorCodes.ERROR_ADD_AUTHORITY_URL_NOT_VALID );
}
return TokenUtils.fromAddAuthToTokenDTO( auth );
}
Where
authorityUrl = https://login.microsoftonline.com/
addTenantId = azureID tenant
addAppId = azureID app ID
addAppScope = /User.Read
for scopes yo can create a new one in "Expose API" option of azure AD app menu
Thanks also to #TonyJu
Related
My Authorization Client: Angular, Resource Server: Java Spring Boot, Authorization Server: Azure Active Directory
I am using oAuth2 to login via Angular via the PKCE Authorization Flow and then pass the token to the back end.
I am able to access the token in my back end via the Authorization Beaer Header, but when I go to use that token to access Microsoft Graph API, I am getting an Invalid token exception.
com.microsoft.graph.http.GraphServiceException: Error code: InvalidAuthenticationToken
Error message: CompactToken parsing failed with error code: 80049217
I am not sure why it is causing this error, because its valid and I can verify via https://jwt.io/
and access my other protected api in postman with the token.
AuthProvider.java
public class AuthProvider implements IAuthenticationProvider {
private String accessToken = null;
public AuthProvider(String accessToken) {
this.accessToken = accessToken;
}
#Override
public void authenticateRequest(IHttpRequest request) {
// Add the access token in the Authorization header
request.addHeader("Authorization", "Bearer " + accessToken);
}
}
SecurityConfiguration.java
http.cors().and()
.authorizeRequests().antMatchers("/home").permitAll()
.and()
.authorizeRequests().antMatchers("/actuator/health").permitAll()
.and()
.authorizeRequests().antMatchers("/**").authenticated()
.and()
.oauth2ResourceServer().jwt();
GraphAPIController.java
private static IGraphServiceClient graphClient = null;
private static AuthProvider authProvider = null;
private static void ensureGraphClient(String accessToken) {
if (graphClient == null) {
// Create the auth provider
authProvider = new AuthProvider(accessToken);
// Create default logger to only log errors
DefaultLogger logger = new DefaultLogger();
logger.setLoggingLevel(LoggerLevel.ERROR);
// Build a Graph client
graphClient = GraphServiceClient
.builder()
.authenticationProvider(authProvider)
.logger(logger)
.buildClient();
}
}
#GetMapping("/getUser")
public static User getUser(#RequestHeader(value="Authorization") String token) {
System.out.println("THE TOKEN: " +token);
ensureGraphClient(token);
// GET /me to get authenticated user
User me = graphClient
.me()
.buildRequest()
.get();
System.out.println("THE USER: " + me);
return me;
}
My Angular Setup:
app.module:
import { OAuthModule } from 'angular-oauth2-oidc';
app.component.ts
Postman:
An access token can only be for one resource. I can see that you configure scope: 'openid api://{appid}/app' in your Angular Setup. It means the access token is for this resource api://{appid}/app rather than Microsoft Graph https://graph.microsoft.com. That is why you got the InvalidAuthenticationToken Error.
So if you want to call Microsoft Graph in your backend API, you need to consider OAuth 2.0 On-Behalf-Of flow. The OAuth 2.0 On-Behalf-Of flow (OBO) serves the use case where an application invokes a service/web API, which in turn needs to call another service/web API.
In your case, your backend API is web API A and Microsoft Graph is web API B.
A sample for your reference.
What I wanna achieve
So I have a client application in java (JavaFX + Spring-boot hybrid-application). You can have a look at it here https://github.com/FAForever/downlords-faf-client . So till now we stored username/ password if the user wished to be kept logged in which is obviously a pretty bad idea. So now I wanna store the refreshtoken and then log the user in with that.
What it looks like now
See here
ResourceOwnerPasswordResourceDetails details = new ResourceOwnerPasswordResourceDetails();
details.setClientId(apiProperties.getClientId());
details.setClientSecret(apiProperties.getClientSecret());
details.setClientAuthenticationScheme(AuthenticationScheme.header);
details.setAccessTokenUri(apiProperties.getBaseUrl() + OAUTH_TOKEN_PATH);
details.setUsername(username);
details.setPassword(password);
OAuth2RestTemplate restTemplate = new OAuth2RestTemplate(details);
restOperations = templateBuilder
// Base URL can be changed in login window
.rootUri(apiProperties.getBaseUrl())
.configure(restTemplate);
What I found so far
I found out that restTemplate.getAccessToken().getRefreshToken() will give me the refreshtoken I want to save and later so to keep the user logged in.
What I can not figure out
I can not find a way to create a OAuth2RestTemplate with an refresh token only. Is that even possible? Can someone point me in the right direction? Maybe link me some articles to read? Is this the right place to read?
I do not think this is possible with an OAuth2RestTemplate, but you can reimplement the desired parts yourself. I'd like to share an example with your for OAuth password login to Microsofts flavour of OAuth2 (Azure Active Directory). It does miss the piece of fetching a new token from an existing refresh token yet, but I added a comment where you need to add it.
A simple way to mimic OAuthRestTemplates behavior is a custom ClientHttpRequestInterceptor which delegates the token fetching to a dedicated Spring service component, that you append to your RestTemplate:
#RequiredArgsConstructor
#Slf4j
public class OAuthTokenInterceptor implements ClientHttpRequestInterceptor {
private final TokenService tokenService;
#NotNull
#Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body,
ClientHttpRequestExecution execution) throws IOException {
request.getHeaders().add("Authorization", "Bearer " + tokenService.getRefreshedToken().getValue());
return execution.execute(request, body);
}
}
This interceptor can be added to your primary RestTemplate:
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<>();
interceptors.add(globalOAuthTokenInterceptor);
restTemplate.setInterceptors(interceptors);
The token service used in the interceptor holds the token in a cache and on request checks for the expiry of the token and if required queries a new one.
#Service
#Slf4j
public class TokenService {
private final TokenServiceProperties tokenServiceProperties;
private final RestTemplate simpleRestTemplate;
private OAuth2AccessToken tokenCache;
public TokenService(TokenServiceProperties tokenServiceProperties) {
this.tokenServiceProperties = tokenServiceProperties;
simpleRestTemplate = new RestTemplateBuilder().
build();
}
public OAuth2AccessToken getRefreshedToken() {
if (tokenCache == null || tokenCache.isExpired()) {
log.debug("Token expired, fetching new token");
tokenCache = refreshOAuthToken();
} else {
log.debug("Token still valid for {} seconds", tokenCache.getExpiresIn());
}
return tokenCache;
}
public OAuth2AccessToken loginWithCredentials(String username, String password) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON_UTF8));
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.add("grant_type", "password");
map.add("resource", tokenServiceProperties.getAadB2bResource());
map.add("client_id", tokenServiceProperties.getAadB2bClientId());
map.add("username", username);
map.add("password", password);
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(map, headers);
return simpleRestTemplate.postForObject(
tokenServiceProperties.getAadB2bUrl(),
request,
OAuth2AccessToken.class
);
}
private OAuth2AccessToken refreshOAuthToken() {
return loginWithRefreshToken(tokenCache.getRefreshToken().getValue());
}
public OAuth2AccessToken loginWithRefreshToken(String refreshToken) {
// add code for fetching OAuth2 token from refresh token here
return null;
}
}
In this code example you would once login using username and password and afterwards all further logins would be using the refresh token. If you want to use the refresh token directly, you use the public method, otherwise it will be done internally.
Since the login code is specifically written for login to Microsoft AAD, you should recheck the MultiValueMap parameters.
TokenServiceProperties are straightforward:
#Data
public class TokenServiceProperties {
private String aadB2bUrl;
private String aadB2bClientId;
private String aadB2bResource;
}
Adapt them if needed.
The whole solution has one minor drawback: Instead of one RestTemplate that you usually fetch via depency injection, you now need a second one (a "simple" one) to fetch the OAuth token. In this example we create it in the constructor of the TokenService. However this is in general bad style as it makes it harder for unit testing etc. You could also think about using qualified beans or using a more basic http client in the TokenService.
Another important thing to note: I am using the spring-security-oauth2 package here. If you did not configure Spring Security in your project, this will trigger Spring Security auto-configuration which might not be desired - you can solve this by excluding undesired packages, e.g. in gradle:
implementation("org.springframework.security.oauth:spring-security-oauth2") {
because "We only want the OAuth2AccessToken interface + implementations without activating Spring Security"
exclude group: "org.springframework.security", module: "spring-security-web"
exclude group: "org.springframework.security", module: "spring-security-config"
exclude group: "org.springframework.security", module: "spring-security-core"
}
I have angular frontend and spring backend. I'm using spring security to hande http basic authentication.
I noticed strange difference in behaviour using advanced rest client(or any other) and angular web app.
For tests I disabled my httpInterceptor so it is not including "Authorisation: Basic foobardoofab==" header. So spring backend should response with Unauthorised error right? Well... Sometimes it does and sometimes in doesn't.
Flow is like this:
I use my authenticate method to log in:
authenticate(credentials, callback) {
const headers = new HttpHeaders(credentials ? {
authorization : 'Basic ' + btoa(credentials.username + ':' + credentials.password)
} : {});
this.http.get('http://localhost:8080/login', {headers: headers}).subscribe(response => {
if (response['name']) {
let authString = 'Basic ' + btoa(credentials.username + ':' + credentials.password);
sessionStorage.setItem('basicauth', authString);
sessionStorage.setItem('username', response['name']);
sessionStorage.setItem('role', response['authorities']);
} else {
// this.authenticated = false;
}
return callback && callback();
});
}
Those values are stored only for getting access inside angular app.
Now I go to http://localhost:4200/events
In this component I have GET request in ngOninit
ngOnInit() {
this.http.get<Array<MyEvent>>('http://localhost:8080/events').subscribe((response) => {
this.events = response;});
}
Because my httpinterceptor is disabled i dont add this "basicauth" stored in session so spring should respond with unathorised. And it does:
{"timestamp":"2019-04-26T13:08:19.167+0000","status":401,"error":"Unauthorized","message":"Unauthorized","path":"/events"}
And I think it is good and expected behaviour.(Since i'm not sending basicauth Im not able to go through spring security.
But there is this second case when im testing it with some rest clients (advanced rest client)
I send GET request to http://localhost:8080/login with "Authorisation: Basic foobar" added to headers and I got 200 OK response from spring.
Now i send get request to http://localhost:8080/events WITHOUT any headers included and I still got 200 OK response and access to returned objects. (While through angular app it respondend with Unauthorised error)
To get unathorised error i have to sent GET request to http://localhost:8080/logout (through angular app im not sending it anywhere)
So my question is why those two cases are different in behaviour?
I will also show my WebSecurityConifg:
#EnableWebSecurity
#Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().httpBasic()
.and()
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS, "/index.html", "/", "/home", "/login").permitAll()
.anyRequest().authenticated().and()
.logout().invalidateHttpSession(true).deleteCookies("JSESSIONID").and()
.csrf().disable();
}
#Bean
CorsConfigurationSource corsConfigurationSource() {
UrlBasedCorsConfigurationSource source = new
UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
return source;
}
}
I'm developing an OAuth2.0 "CLIENT" application which call some APIs(secured by oauth2.0).
I'm using OAuth2.0RestTemplate which contains CLIENT_ID, CLIENT_SECRET, username and password. The code for calling OAuth2.0 secured APIs looks like this:
#Bean
OAuth2ProtectedResourceDetails resource() {
ResourceOwnerPasswordResourceDetails resource = new ResourceOwnerPasswordResourceDetails();
List<String> Scopes = new ArrayList<String>(2);
Scopes.add("read");
Scopes.add("write");
resource.setClientAuthenticationScheme(AuthenticationScheme.header);
resource.setId("*****");
resource.setAccessTokenUri(tokenUrl);
resource.setClientId("*****");
resource.setClientSecret("*****");
resource.setGrantType("password");
resource.setScope(Scopes);
resource.setUsername("*****");
resource.setPassword("*****");
return resource;
}
#Autowired
private OAuth2RestTemplate restTemplate;
Map<String, String> allCredentials = new HashMap<>();
allCredentials.put("username", "***");
allCredentials.put("password", "***");
restTemplate.getOAuth2ClientContext().getAccessTokenRequest().setAll(allCredentials);
ParameterizedTypeReference<List<MyObject>> responseType = new ParameterizedTypeReference<List<MyObject>>() { };
ResponseEntity<List<MyObject>> response = restTemplate.exchange("https://***.*****.com/api/*****/*****",
HttpMethod.GET,
null,
responseType);
AllCities all = new AllCities();
all.setAllCities(response.getBody());
As you can see everytime I want to call a service the code get a new ACCESS TOKEN which is wildly wrong!!! My question is how can I automatically receive and store the issued token in my application an use it until it expires and then automatically get a new one?
On the other hand my token only contains access token and doesn't contain refresh token(I don't know why!!! this is so weird!!!)
Hello you can design like google client library.
First step you need to create the datastore for store the token in your directory like C:/User/soyphea/.token/datastore.
Before you load your function retrieve access_token_store. Your access token should have expired_in.
if(access_token_store from your datastore !=null && !expired){
access_token = access_token_store.
} else {
access_token = Your RestTemplate function for retrieve access_token.
}
finally you can retrieve access_token.
In spring security oauth2 if you want to support refresh_token you need to set,
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("resource-serv")
.scopes("read")
.resourceIds("my-resource")
.secret("secret123")
.and()
.withClient("app")
.authorizedGrantTypes("client_credentials", "password", "refresh_token")
.scopes("read")
.resourceIds("my-resource")
.secret("appclientsecret");
}
First of all you have define that your app is a Oaut2App for this in Spring boot you can use the annotation #EnableOAuth2Client in your code and configure the client application metadata in your applicaition.yml. A skeleton client app can be like below:
#EnableOAuth2Client
#SpringBootApplication
public class HelloOauthServiceApplication {
public static void main(String[] args) {
SpringApplication.run(HelloOauthServiceApplication.class, args);
}
#Bean
public OAuth2RestTemplate oAuth2RestTemplate(OAuth2ProtectedResourceDetails resource){
return new OAuth2RestTemplate(resource);
}
}
application.yml
security:
oauth2:
client:
clientId: client
clientSecret: secret
accessTokenUri: http://localhost:9090/oauth/token
userAuthorizationUri: http://localhost:9090/oauth/authorize
auto-approve-scopes: '.*'
registered-redirect-uri: http://localhost:9090/login
clientAuthenticationScheme: form
grant-type: passwordR
resource:
token-info-uri: http://localhost:9090/oauth/check_token
in this way you have guarantee that the OAuth2RestTemplate of spring will use and upgrade the token
This code is only for a user.
I'm looking for the way to make this for multiple user.
Please, give me some tips.
To run the batch job, I know that some variables (is_authorized, requestToken and accessToken) should be removed. I tried to use spring-social-tumblr(on github)but it was not easy to use ConnectionRepository. so I tried to use signpost.
After signing with signpost, how could I set the user access token for multi-user?
Is it right to use OAuthConsumer class?
#Controller
public class TumblrProfileController {
private OAuthService service;
private Token requestToken; //should be removed for multiuser
private Token accessToken; // same above
private static final String PROTECTED_RESOURCE_URL = "http://api.tumblr.com/v2/user/info";
#Autowired
private JobLauncher jobLauncher;
#Autowired
private Job job;
#Inject
private ConnectionRepository connectionRepository;
Logger log = LoggerFactory.getLogger(this.getClass());
private boolean is_authorized = false;
#RequestMapping(value = "/tumblr/webrequest", method = RequestMethod.GET)
public String home(OAuthConsumer user, Model model) {
final String PROTECTED_RESOURCE_URL = "http://api.tumblr.com/v2/user/info";
service = new ServiceBuilder().provider(TumblrApi.class).apiKey("clientKey") .apiSecret("secretKey").callback("http://localhost:8080/pen/tumblr/login").build();
log.info("Fetching the Request Token...");
// Obtain the Request Token
requestToken = service.getRequestToken();
log.info("Now go and authorize Scribe here:");
String redirectUrl = service.getAuthorizationUrl(requestToken);
log.info(redirectUrl);
return "redirect:" + redirectUrl;
}
#RequestMapping(value = "/tumblr/login", method = RequestMethod.GET)
public String login(#RequestParam(required = false) final String oauth_verifier) {
Verifier verifier = new Verifier(oauth_verifier);
// Trade the Request Token and Verfier for the Access Token
log.info("Trading the Request Token for an Access Token...");
accessToken = service.getAccessToken(requestToken, verifier);
log.info("Got the Access Token!");
log.info("(if your curious it looks like this: " + accessToken + " )");
// Now let's go and ask for a protected resource!
log.info("Now we're going to access a protected resource...");
OAuthRequest request = new OAuthRequest(Verb.GET, PROTECTED_RESOURCE_URL);
service.signRequest(accessToken, request);
Response response = request.send();
log.info("Got it! Lets see what we found...");
log.info(response.getBody());
log.info("Thats it man! Go and build something awesome with Scribe! :)");
run();
is_authorized = true;
return "tumblr/feed";
}
public void run() {
try {
if(! is_authorized ) return;
OAuthRequest request = new OAuthRequest(Verb.GET, PROTECTED_RESOURCE_URL);
service.signRequest(accessToken, request);
Response response = request.send();
log.info("[2nd Call ]Got it! Lets see what we found...");
log.info(response.getBody());
} catch (Exception e) {
e.printStackTrace();
}
}
Although I've not used Spring Social Tumblr (it's a community-led project), the process shouldn't be much (or any) different than using Spring Social with Facebook or Twitter.
Note that in this code, you're doing too much work. You're going to the trouble of redirecting to Tumblr for authorization and then handling the redirect to exchange the request token and verifier for an access token. Certainly, those things must be done, but with Spring Social there's absolutely no reason why you have to do those things. That's what ConnectController is for. ConnectController handles all of that, creates and persists the connection, and (generally speaking) you never have to muck about with OAuth directly. And, it has no problem working with multiple users.
May I recommend that you look at the Spring Social Showcase example at https://github.com/spring-projects/spring-social-samples/tree/master/spring-social-showcase to see how it's done? That example connects with Facebook, Twitter, and LinkedIn, but there's really no reason why it couldn't connect to Tumblr in the same fashion. For a much simpler approach that leverages Spring Boot and automatic configuration, you might also have a look at https://github.com/spring-projects/spring-social-samples/tree/master/spring-social-showcase-boot. (Note, however, that Spring Boot doesn't have autoconfig for Tumblr, so there'd still be some manual config required.)