tl;dr
Are there security issues with your approach?
Difficult to say, as it is not clear from the question which token you are sending in the mail. You should read my long answer to understand my concerns. But, in short: if you send the guid, then yes, there is an issue (see the 'implementation' part of my answer. If, on the other hand, you send the refresh token: then there probably is no security issue.
Preface
As pointed out by @CBHacking:
the intended use of OAuth2 refresh tokens is for persistent access; an app gets a refresh token, and as long as it has a valid refresh token and the session is not invalidated on the server, the app is authenticated. The access token is short-lived but, if you're using refresh tokens at all, you expect the client to remain authenticated for a long time
Anyway, it does not really matter. If you want to use the refresh token only once, then you are free to do so.
An example of the implementation you want to achieve
I'm not sure if you already know it, but Slack has actually the sign-in experience that you are describing. You can test it yourself by creating an account and install their mobile app, or just see how it works here. Their link looks like this:
https://slack.com/z-app-0000000000-0000000000-AAAAAAAAAA (I've replaced numbers with 0's and letters with A's). I assume that their link is built-up as follows (note: I don't know if this is true, it's an assumption):
- First part: user id
- Second part: time
- Third part: unique token
Clicking this link will redirect you to somewhere like this:
slack://login/XXXXXXXXX/xoxo-0000000000-24characterlongtoken
So, another token is created which is then used to open the application.
Implementation
Your guid implementation seems a bit overengineered for the following reasons:
- Why make use of an app-generated guid, if you could just as well directly send a 'magic link' to the mailaddress of the user from the server? Your security lies with the security of the user's access to the mailbox, not with the guid (which is not secure anyway). I agree that you need some kind of token, more about that later.
- You mention that this guid is then encoded in the refreshtoken as a claim. I assume you mean adding it to the protectedticket of the user (the refreshtoken itself does not contain claims, in contrast to the access token).
- And then, I am a bit lost in your implementation. You mention that you send the refresh token to the user, but really a refresh token is only a reference to a protected ticket in your database. So you then use this refresh token to retrieve the guid. And then what? You see, the guid is of no use in this set-up. The only token that matters in this set-up is the refresh token.
Just send a token
Sidenote, I make use of the following notations:
- access token: the access token as described by oauth
- refresh token: the refresh token as described by oauth
- unique token: any token that is unique. If you want to use the refresh token for this, it's your choice. I give other options at the end of this answer.
So, you actually provided your own answer. We get rid of the guid and do it as follows:
- The user requests a magic link on the application.
- On the server, you check if the emailaddress is known, if so, create a unique token for this user and send it in a mail (link to an endpoint that can verify the unique token). You probably also should send some kind of user identifier.
- When the user clicks the link, you verify the unique token (for that user) and issue a new access token and refresh token.
Now, as mentioned before, a refresh token really is considered a long-lived token. In some implementations, this token even never expires. To prevent access, you should revoke it instead. Therefore, it does not really make sense that you would use a refresh token as a unique token in the email.
Even better would be to create a unique token that is uniquely tied to the user. This depends on your implementation, but in .NET you could for example use GenerateUserTokenAsync. When using this, you don't even need to store the token in database, when the login request comes in, you can just call VerifyUserTokenAsync and you are sure that the user is the person you sent the mail to.
If you want to create a custom unique token instead, you should of course link it somehow to your user in the database. You could generate such a cryptographically secure unique token yourself. Depending on your programming language, the implementation differs. Google for it, and you will get answers.