After the JWT returned from Sign-in with Apple expires, firebase cannot locate user profile based on new JWT - firebase-authentication

So my issue is that after a user signs in with Apple and creates their account, if they do not use the application for 24+ hours they are signed out. When the user returns and the sign-in flow is initiated, firebase is not finding the existing user account based on the credential created from the Apple JWT. This results in a new account and the use not being able to access their old account.
I followed the instructions from this firebase video
logger.debug("Apple sign-in: initated")
switch result {
case .success(let authResults):
switch authResults.credential {
case let appleIDCredential as ASAuthorizationAppleIDCredential:
guard !currentNonce.isEmpty else {
fatalError("Apple sign-in: A login callback was received, but no login request was sent.")
}
guard let appleIDToken = appleIDCredential.identityToken else {
logger.error("Apple sign-in: Unable to fetch identity token")
return
}
guard let idTokenString = String(data: appleIDToken, encoding: .utf8) else {
logger.error("Apple sign-in: Unable to serialize token string from data: \(appleIDToken.debugDescription)")
return
}
// This code is to comply with Apples new rules about account deletion
if let authorizationCode = appleIDCredential.authorizationCode, let codeString = String(data: authorizationCode, encoding: .utf8) {
let url = URL(string: "[redacted]/getRefreshToken?code=\(codeString)".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "https://apple.com")!
let task = URLSession.shared.dataTask(with: url) {(data, response, error) in
if let data = data {
let refreshToken = String(data: data, encoding: .utf8) ?? ""
// *For security reasons, we recommend iCloud keychain rather than UserDefaults.
UserDefaults.standard.set(refreshToken, forKey: "refreshToken")
UserDefaults.standard.synchronize()
print("refresh token: \(refreshToken)")
print("refreshToken set: \(refreshToken)")
}
}
task.resume()
}
print("idTokenString: \(idTokenString), nonce: \(currentNonce)")
let credential = OAuthProvider.credential(withProviderID: "apple.com",
idToken: idTokenString,
rawNonce: currentNonce)
logger.debug("Apple sign-in: OAuthProvider credential created")
Task {
await authViewModel.signInWithApple(credential: credential)
}
default:
logger.debug("Apple sign-in: failed to cast \(authResults.credential.description) to ASAuthorizationAppleIDCredential")
break
}
case .failure(let error):
logger.error("Apple sign-in: failed - \(error.localizedDescription)")
}
}

Related

OAuth 2Client: Invalid token signature

I wanted to handle my user auth by google.
async verify(token) {
try {
const ticket = await client.verifyIdToken({
idToken:token,
audience: '245409008225-isc00em81fk0vs423pm4jmgc2hcma5jj.apps.googleusercontent.com',
});
const payload = ticket.getPayload();
return payload
} catch (error) {
console.log(error)
}
this code works fine, only for first time to create user in DB. And i save this token to localstorage and retrieve it every time to validate that user is authentificated. Here is my code:
async isAuth(token) {
if (!token) {
false
}
const userData = tokenService.verify(token);
const tokenFromDb = await tokenService.findToken(token);
if (!userData || !tokenFromDb) {
throw ApiError.UnAuthorizedError();
}
const user = await User.findOne({where: {email: userData.email}});
await tokenService.saveToken(token);
return true;
}
I did google, and i supposed to define jwk key for google auth api? But I can't find real solution. So, hope you guys can help me. I never used before google auth. For now I have this solution by making request to this api https://www.googleapis.com/oauth2/v1/tokeninfo?id_token=token and getting from there my user email

How to use OAuth Authorization Code for CLIs

Trying to allow a CLI I'm developing to "login" via web browser and obtain an access token for the user's account, similar to how gcloud and github's CLIs do it. I realize it'll be using the OAuth Authorization Code flow.
But what about the client_secret?
I've found out that github cli just doesn't care about exposing it, and it's right there in the source code: https://github.com/cli/cli/blob/6a8deb1f5a9f2aa0ace2eb154523f3b9f23a05ae/internal/authflow/flow.go#L25-L26
Why is this not a problem? or is it?
I'm not yet using OAuth for the cli's login
STANDARDS
The CLI app is a native public client and should use authorization code flow + PKCE rather than a fixed client secret. It should also follow the flow from
RFC8252 and receive the browser response using a local HTTP (loopback) URI.
THIS IMPLEMENTATION
Looks like the github code here uses a client secret and does not use PKCE. You may have to provide a client secret if using this library, but it cannot be kept secret from users. Any user could easily view it, eg with an HTTP proxy tool.
CODE
If the infrastructure enables you to follow the standards, aim for something similar to this Node.js code.
* The OAuth flow for a console app
*/
export async function login(): Promise<string> {
// Set up the authorization request
const codeVerifier = generateRandomString();
const codeChallenge = generateHash(codeVerifier);
const state = generateRandomString();
const authorizationUrl = buildAuthorizationUrl(state, codeChallenge);
return new Promise<string>((resolve, reject) => {
let server: Http.Server | null = null;
const callback = async (request: Http.IncomingMessage, response: Http.ServerResponse) => {
if (server != null) {
// Complete the incoming HTTP request when a login response is received
response.write('Login completed for the console client ...');
response.end();
server.close();
server = null;
try {
// Swap the code for tokens
const accessToken = await redeemCodeForAccessToken(request.url!, state, codeVerifier);
resolve(accessToken);
} catch (e: any) {
reject(e);
}
}
}
// Start an HTTP server and listen for the authorization response on a loopback URL, according to RFC8252
server = Http.createServer(callback);
server.listen(loopbackPort);
// Open the system browser to begin authentication
Opener(authorizationUrl);
});
}
/*
* Build a code flow URL for a native console app
*/
function buildAuthorizationUrl(state: string, codeChallenge: string): string {
let url = authorizationEndpoint;
url += `?client_id=${encodeURIComponent(clientId)}`;
url += `&redirect_uri=${encodeURIComponent(redirectUri)}`;
url += '&response_type=code';
url += `&scope=${scope}`;
url += `&state=${encodeURIComponent(state)}`;
url += `&code_challenge=${encodeURIComponent(codeChallenge)}`;
url += '&code_challenge_method=S256';
return url;
}
/*
* Swap the code for tokens using PKCE and return the access token
*/
async function redeemCodeForAccessToken(responseUrl: string, requestState: string, codeVerifier: string): Promise<string> {
const [code, responseState] = getLoginResult(responseUrl);
if (responseState !== requestState) {
throw new Error('An invalid authorization response state was received');
}
let body = 'grant_type=authorization_code';
body += `&client_id=${encodeURIComponent(clientId)}`;
body += `&redirect_uri=${encodeURIComponent(redirectUri)}`;
body += `&code=${encodeURIComponent(code)}`;
body += `&code_verifier=${encodeURIComponent(codeVerifier)}`;
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body,
};
const response = await fetch(tokenEndpoint, options);
if (response.status >= 400) {
const details = await response.text();
throw new Error(`Problem encountered redeeming the code for tokens: ${response.status}, ${details}`);
}
const data = await response.json();
return data.access_token;
}

How to redirect from GraphQL middleware resolver on authentication fail?

Introduction:
I am using GraphQL Mesh as a gateway between my app and an API. I use Apollo Client as the GraphQL client. When a user wants to visit the first screen after hitting the log-in button, I do a query to load data from a CMS. This query has to go through the gateway. In the gateway I do an auth check to see if the user has a valid JTW access token, if not, I want to redirect back to the sign-in page. If the user has a token, he is let through.
The gateway is-auth.ts resolver:
const header = context.headers.authorization;
if (typeof header === "undefined") {
return new Error("Unauthorized: no access token found.");
} else {
const token = header.split(" ")[1];
if (token) {
try {
const user = jwt.verify(token, process.env.JWT_SECRET as string);
} catch (error) {
return new Error("Unauthorized: " + error);
}
} else {
return new Error("Unauthorized: no access token found.");
}
}
return next(root, args, context, info);
},
Problem: Right now, I am returning Errors in the authentication resolver of the gateway, hoping that I could pick them up in the error object that is sent to Apollo Client and then redirect off of that. Unfortunately, I don't get that option, since the Errors are thrown immediately, resulting in an error screen for the user (not what I want). I was hoping this would work in order to redirect to the sign-in from the client-side, but it does not work:
const { data, error } = await apolloClient(accessToken).query({
query: gql`
query {
...where my query is.
}
`,
});
if (error) {
return {
redirect: {
permanent: false,
destination: `/sign-in`,
},
};
}
Does anyone perhaps have a solution to this problem?
This is the GraphQL Mesh documentation on the auth resolver, for anyone that wants to see it: https://www.graphql-mesh.com/docs/transforms/resolvers-composition. Unfortunately, it doesn't say anything about redirects.
Kind regards.

Access Youtube Channel ID once signed in though Firebase GoogleAuthProvider

I've setup a basic Firebase authentication app which uses Google. I've passed the following scopes:
https://www.googleapis.com/auth/youtube.force-ssl
When logging in, it states that it is gaining permission to manage my Youtube Account, but the response I get back has nothing relevant to Youtube in it, such as a channelId.
Even when doing a simple $http.get request against the logged in accounts Youtube subscriptions I get the following response:
The request uses the <code>mine</code> parameter but is not properly authorized.
So would I need to login through Google, then authenticate again once signed in to access my Youtube account?
Sample login:
var provider = new firebase.auth.GoogleAuthProvider();
provider.addScope("https://www.googleapis.com/auth/youtube.force-ssl");
$scope.login = function () {
Auth.$signInWithPopup(provider).then(function (result) {
console.log(result);
console.log("Signed in as:", result.user.uid);
}).catch(function (error) {
console.error("Authentication failed:", error);
});
}
Apologies in the delay.
Here is how I solved this problem. When logging in using Firebase with Google as a provider, I get the access_token given by Google and query YouTubes API to get the correct channel.
An example of my login function is below:
this.loginMainGoogle = function (event) {
gapi.auth2.getAuthInstance().signIn().then(function _firebaseSignIn(googleUser) {
var unsubscribe = $rootScope.authObj.$onAuthStateChanged(function (firebaseUser) {
unsubscribe();
// Check if we are already signed-in Firebase with the correct user.
if (!_isUserEqual(googleUser, firebaseUser)) {
// Build Firebase credential with the Google ID token.
console.log(googleUser.getAuthResponse());
var credential = firebase.auth.GoogleAuthProvider.credential(
googleUser.getAuthResponse().id_token);
// Sign in with credential from the Google user.
return $rootScope.authObj.$signInWithCredential(credential)
.then(function (result) {
var ytToken = googleUser.getAuthResponse().access_token;
localStorage.setItem('gToken', ytToken);
$rootScope.tokenerino = ytToken;
$http.get("https://www.googleapis.com/youtube/v3/channels?part=id&mine=true&access_token=" + ytToken)
.then(function(response) {
$rootScope.myChan = response.data.items[0].id;
localStorage.setItem('myChannelId', $rootScope.myChan);
updateYTChannel(result.uid, response.data.items[0].id);
});
$rootScope.currentLoginStatus = true;
$rootScope.notification("You Have Signed In");
//Don't redirect them if they login via a YouTube playlist
if ($location.path().indexOf('playlists') !== 1) {
$state.go('mymusic');
}
}, function errorCallback(error) {
console.log(error);
});
}
})
});
}
I store the Channel for the user in Firebase, but you can put it in localStorage if you want. The only problem is that the access_token only lasts for 1 hour. Hopefully this helps anyone and if a better solution has been found - feel free to share!

Refresh token and roles are missing (OpenIddict)

my tokens are missing refresh and role property. I am using OpenIddict. The code did work until today and it still works on home computer, but not on work.
I am pretty sure I did something wrong, but since I compare startup.cs, AuthorizationController.cs and they are the same (work and home), I need some help what could be the source of problem.
I need to get roles for user which logins, because my Angular2 application needs to know what a user can do on web page.
Request I sent:
Work response:
Home response:
Startup code (again same on home computer):
services.AddOpenIddict<int>()
.AddEntityFrameworkCoreStores<AppDbContext>()
.AddMvcBinders()
.EnableTokenEndpoint("/API/authorization/token")
.AllowPasswordFlow()
.AllowRefreshTokenFlow()
.UseJsonWebTokens()
.AddEphemeralSigningKey() //todo naj bi bil pravi certifikat, če odstranič to vrstico ne dela in vidiš error.
.SetAccessTokenLifetime(TimeSpan.FromMinutes(30))
.SetRefreshTokenLifetime(TimeSpan.FromDays(14))
.DisableHttpsRequirement();
Controller code (again: same on home computer):
public class AuthorizationController : BaseController
{
public AuthorizationController(AppDbContext context, OpenIddictApplicationManager<OpenIddictApplication<int>> applicationManager, SignInManager<AppUser> signInManager, UserManager<AppUser> userManager) : base(context, applicationManager, signInManager, userManager)
{
}
[Authorize, HttpGet("authorize")]
public async Task<IActionResult> Authorize(OpenIdConnectRequest request)
{
Debug.Assert(request.IsAuthorizationRequest(),
"The OpenIddict binder for ASP.NET Core MVC is not registered. " +
"Make sure services.AddOpenIddict().AddMvcBinders() is correctly called.");
// Retrieve the application details from the database.
var application = await applicationManager.FindByClientIdAsync(request.ClientId, HttpContext.RequestAborted);
if (application == null)
{
return View("Error", new ErrorViewModel
{
Error = OpenIdConnectConstants.Errors.InvalidClient,
ErrorDescription = "Details concerning the calling client application cannot be found in the database"
});
}
// Flow the request_id to allow OpenIddict to restore
// the original authorization request from the cache.
return View(new AuthorizeViewModel
{
ApplicationName = application.DisplayName,
RequestId = request.RequestId,
Scope = request.Scope
});
}
[HttpPost("token"), Produces("application/json")]
public async Task<IActionResult> Exchange(OpenIdConnectRequest request)
{
Debug.Assert(request.IsTokenRequest(),
"The OpenIddict binder for ASP.NET Core MVC is not registered. " +
"Make sure services.AddOpenIddict().AddMvcBinders() is correctly called.");
if (request.IsPasswordGrantType())
{
var user = await userManager.FindByNameAsync(request.Username);
if (user == null)
{
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The email/password couple is invalid."
});
}
// Ensure the user is allowed to sign in.
if (!await signInManager.CanSignInAsync(user))
{
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The specified user is not allowed to sign in."
});
}
// Reject the token request if two-factor authentication has been enabled by the user.
if (userManager.SupportsUserTwoFactor && await userManager.GetTwoFactorEnabledAsync(user))
{
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The specified user is not allowed to sign in."
});
}
// Ensure the user is not already locked out.
if (userManager.SupportsUserLockout && await userManager.IsLockedOutAsync(user))
{
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The username/password couple is invalid."
});
}
// Ensure the password is valid.
if (!await userManager.CheckPasswordAsync(user, request.Password))
{
if (userManager.SupportsUserLockout)
{
await userManager.AccessFailedAsync(user);
}
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The username/password couple is invalid."
});
}
if (userManager.SupportsUserLockout)
{
await userManager.ResetAccessFailedCountAsync(user);
}
// Create a new authentication ticket.
var ticket = await CreateTicketAsync(request, user);
return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
}
else if (request.IsRefreshTokenGrantType())
{
// Retrieve the claims principal stored in the refresh token.
var info = await HttpContext.Authentication.GetAuthenticateInfoAsync(
OpenIdConnectServerDefaults.AuthenticationScheme);
// Retrieve the user profile corresponding to the refresh token.
var user = await userManager.GetUserAsync(info.Principal);
if (user == null)
{
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The refresh token is no longer valid."
});
}
// Ensure the user is still allowed to sign in.
if (!await signInManager.CanSignInAsync(user))
{
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The user is no longer allowed to sign in."
});
}
// Create a new authentication ticket, but reuse the properties stored
// in the refresh token, including the scopes originally granted.
var ticket = await CreateTicketAsync(request, user, info.Properties);
return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
}
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.UnsupportedGrantType,
ErrorDescription = "The specified grant type is not supported."
});
}
private async Task<AuthenticationTicket> CreateTicketAsync(
OpenIdConnectRequest request, AppUser user,
AuthenticationProperties properties = null)
{
// Create a new ClaimsPrincipal containing the claims that
// will be used to create an id_token, a token or a code.
var principal = await signInManager.CreateUserPrincipalAsync(user);
// Note: by default, claims are NOT automatically included in the access and identity tokens.
// To allow OpenIddict to serialize them, you must attach them a destination, that specifies
// whether they should be included in access tokens, in identity tokens or in both.
foreach (var claim in principal.Claims)
{
// In this sample, every claim is serialized in both the access and the identity tokens.
// In a real world application, you'd probably want to exclude confidential claims
// or apply a claims policy based on the scopes requested by the client application.
claim.SetDestinations(OpenIdConnectConstants.Destinations.AccessToken,
OpenIdConnectConstants.Destinations.IdentityToken);
}
// Create a new authentication ticket holding the user identity.
var ticket = new AuthenticationTicket(principal, properties,
OpenIdConnectServerDefaults.AuthenticationScheme);
if (!request.IsRefreshTokenGrantType())
{
// Set the list of scopes granted to the client application.
// Note: the offline_access scope must be granted
// to allow OpenIddict to return a refresh token.
ticket.SetScopes(new[] {
OpenIdConnectConstants.Scopes.OpenId,
OpenIdConnectConstants.Scopes.Email,
OpenIdConnectConstants.Scopes.Profile,
OpenIdConnectConstants.Scopes.OfflineAccess,
OpenIddictConstants.Scopes.Roles
}.Intersect(request.GetScopes()));
}
ticket.SetResources("OpPISWeb"); //also in startup.cs
return ticket;
}
}
For decoding id_token I am using angular-jwt:
return this.http.post('api/authorization/token', this.encodeObjectToParams(data), options)
.map(res => res.json())
.map((tokens: AuthTokenModel) =>
{
console.log("loged in", tokens);
let now = new Date();
tokens.expiration_date = new Date(now.getTime() + tokens.expires_in * 1000).getTime().toString();
localStorage.setItem('id_token', tokens.access_token);
localStorage.setItem('refresh_token', tokens.refresh_token);
const profile = this.jwtHelper.decodeToken(tokens.id_token) as ProfileModel;
const roles: string[] = typeof profile.role === "string" ? [profile.role] : profile.role;
const userProfile: Profile = new Profile(parseInt(profile.sub), roles);
localStorage.setItem('profile', JSON.stringify(userProfile));
this.refreshTokens(tokens.expires_in * 1000 * 0.8);
return profile;
});
The behavior you're seeing was caused by a bug introduced Friday. I fixed it a few minutes ago and new packages are being published at this moment.
Thanks for reporting it.