This is a pretty common situation you find yourself in which is a good thing as it means the solution is well known, discussed and tested.
- Is a simple signature verification by App2 is enough? Or there is a need that it sends the token to App1 for verification (there is a white list of public keys for App2) ?
Correct, a "simple" signature verification is all that App2 needs to do to validate the token. This will guarantee a.) the token was dispensed by App1 and b.) the claims (key-value pairs) in the token have not been tampered with.
- I'm considering to add an expiration date for 30 mins, how should I handle refresh token at the client-side ?
This comes more down to personal preference, but I do not like refresh tokens. Refresh tokens imply a distant future expiry date (i.e. couple days or a week) and can be very damaging if they are stolen. Furthermore, in my experience, there is no truly good way to store a secret on the client side. Cookies are insecure and I do not believe client side DB's are much better. You could encrypt it with user password but that requires the user to re-type the password which defeats the purpose of the refresh token anyways, right? So I would avoid refresh tokens if possible.
- Instead of generation refresh tokens, is it secure to say that the access token is one-time only and App2 creates a session on the server-side (using the data in the token)?
I do not see any flaws in this plan, I think it is much more secure than relying on a refresh token. The only caveat here is to ensure there is an ability to void a access token on App1. For other words, if I authenticate against App1, get my token then come to App2, authenticate and get my session ID, I am happy. But, an attacker could steal my token from my browsers cookie. The attacker can use this token to go and get his own session with my credentials at App2 as long as the token is not yet expired. Therefore, App1 and App2 would have to be able to communicate in this scenario. App2 checks that App1 says the token has not been used yet. App2 uses it, then tells App1 it was used, please void the token so that it cannot be re-used in future requests to generate another session ID.
As you can see, that gets a lot more complicated. But, if you do not do that, you are not really using one-time tokens. An alternative would be you make the token expiry VERY short (i.e. five minutes or less), enough time for the user to be re-directed and authenticate with App2 and get their session ID for continued authentication. This might be the simpler route but it really is dependent on the level of security you wish to reach here.
Either way, the Session ID is a smart idea. It would likely reduce the load on your App2 server as it will not have to constantly re-check token signatures for every request.