OneDrive / onedrive-sdk-csharp

OneDrive SDK for C#! https://dev.onedrive.com
Other
294 stars 143 forks source link

How to authenticate in an ASP.NET app? #23

Closed opcodewriter closed 8 years ago

opcodewriter commented 8 years ago

How to authenticate in an ASP.NET app with OneDrive API? I tried to use Live SDK for ASP.NET to discover it's not working anymore, and that I should use the new OneDrive API. A bit confusing, any help would be great... For authentication, I know I could use JavaScript like in the sample, but it would be great to have .NET support for ASP.NET, as I intend to upload / download files to/from OneDrive.

Another question: In ASP.NET I can use OWIN's built in MicrosoftAccountAuthenticationExtensions::UseMicrosoftAccountAuthentication Would it be possible to use this identity to login to OneDrive and upload\download files? It would be useful to use the ASP.NET identity support which already manages and saves the authentication in database.

Thanks!

ginach commented 8 years ago

You have a couple options:

  1. Use the default auth flow. This requires implementing IWebAuthenticationUi for displaying the authentication UI to the user and passing that into the client. It looks like an extension method is missing, I'll make a note to fix that, but you can do this:

    var client = OneDriveClient.GetMicrosoftAccountClient( appId, returnUrl, scopes, clientSecret, serviceInfoProvider: new ServiceInfoProvider(webAuthenticationUi));

  2. Create your own authentication provider implementation that wraps MicrosoftAccountAuthenticationExtensions::UseMicrosoftAccountAuthentication. To do that, you can either implement your own IAuthenticationProvider or inherit from AuthenticationProvider. Inheriting from AuthenticationProvider allows you to use the credential caching system we have in the SDK. Here is an example of one of our implemented authentication providers. You can then create the client like this:

    var client = OneDriveClient.GetMicrosoftAccountClient( appId, returnUrl, scopes, clientSecret, serviceInfoProvider: new ServiceInfoProvider(authenticationProvider));

After creating the client you call await client.AuthenticateAsync() and you'll be good to make requests with it.

opcodewriter commented 8 years ago

Please correct me if I'm wrong, I don't see how it would be possible to use IWebAuthenticationUi in an ASP.NET MVC app. On server, when calling AuthenticateAsync(), this is going to call IWebAuthenticationUi implementation. But since showing the login UI is on client side, there's no way to communicate the results to the IWebAuthenticationUi instance, the instance is gone.

ginach commented 8 years ago

The feasibility of option 1 differs depending on the app. It sounds like it's not an option for yours so you'll probably want to look into option 2.

opcodewriter commented 8 years ago

Can the access token returned by authenticating with Microsoft account be used to call OneDrive API? I don't understand how the Microsoft signin and OneDrive sigin work together..

I'm still lost... I spent time looking at AuthenticationProvider, MicrosoftAccountAuthenticationProvider implementation and other classes. I understand the idea, but I just don't know how to use all together, what I need to implement.. Is there any sample which does it for an ASP.NET app?

ginach commented 8 years ago

We don't have a sample currently but are working on putting one together. I'll respond when we have something.

mybluedog commented 8 years ago

I think I have a similar problem. I get an offline refresh_token in one system using REST. Then I have a C# client on another system that needs to authenticate using just the refresh_token and the client_id (and redirect_uri and scope, and whatever else OAuth demands). Can this be done?

ginach commented 8 years ago

Yes, it can. There's a bug (IAuthenticationProvider doesn't have the CurrentAccountSession setter anymore) that I'll be putting a fix out for tomorrow, but after the fix you can do the following for an authenticated client in the above scenario:

var client = OneDriveClient.GetMicrosoftAccountClient(
    client_id,
    redirect_uri,
    scopes);

client.AuthenticationProvider.CurrentAccountSession = new AccountSession
{
    RefreshToken = "token"
};

await this.oneDriveClient.AuthenticateAsync();

I understand the calling pattern here is a little bit awkward, but it'll accomplish what you want.

The reason why this works is the authentication provider goes through the following flow:

  1. Is there a current account session that isn't expiring? If so, return it.
  2. If not, do we have a refresh token? If so, use it to get a new auth token using the information the client has about the app.
  3. If the above didn't work and we have an IWebAuthenticationUi implementation use it to authenticate the user via the UI flow.
  4. If all of the above didn't work, return null.
mybluedog commented 8 years ago

Sounds good. We'll try it when it's ready.

ginach commented 8 years ago

@mybluedog The new package is now up, v 1.1.9.

mybluedog commented 8 years ago

Thanks for your help with this.

Unfortunately, in the code above, client.AuthenticationProvider is null and there's no setter for it, so the AccountSession can't be set. I decided to implement my own AuthenticationProvider like this...

using OD = Microsoft.OneDrive.Sdk;

namespace
{
    class AuthenticationProvider : OD.IAuthenticationProvider
    {
        OD.AccountSession m_session;

        public AuthenticationProvider(string refreshToken)
        {
            m_session = new OD.AccountSession()
            {
                RefreshToken = refreshToken,
                ExpiresOnUtc = DateTime.SpecifyKind(DateTime.MaxValue, DateTimeKind.Utc),
                ClientId = "{clientId}",
                Scopes = new[] { "wl.signin", "wl.offline_access", "onedrive.readonly" },
                AccountType = OD.AccountType.MicrosoftAccount
            };
        }

        public OD.AccountSession CurrentAccountSession
        {
            get
            {
                return m_session;
            }

            set
            {
                throw new NotImplementedException();
            }
        }

        public Task AppendAuthHeaderAsync(HttpRequestMessage request)
        {
            request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue(OD.Constants.Headers.Bearer, m_session.AccessToken);
            return Task.WhenAll(); // == Task.CompletedTask
        }

        public Task<OD.AccountSession> AuthenticateAsync()
        {
            return Task<OD.AccountSession>.FromResult(m_session);
        }

        public Task SignOutAsync()
        {
            throw new NotImplementedException();
        }
    }
}

And I'm creating my client like this...

OD.IOneDriveClient oneDriveClient = OD.OneDriveClient.GetMicrosoftAccountClient(
    clientId,
    "https://login.live.com/oauth20_desktop.srf", // For now, I've created the refresh_token using this return URI
    new[] { "wl.signin", "wl.offline_access", "onedrive.readonly" },
    clientSecret,
    serviceInfoProvider: new OD.ServiceInfoProvider(
        new AuthenticationProvider(@"{refresh_token}")
        )
    );

OD.AccountSession session = oneDriveClient.AuthenticateAsync().Result;
return (oneDriveClient != null && oneDriveClient.IsAuthenticated) ? oneDriveClient : null; // IsAuthenicatied == true

But the problem is that even though oneDriveClient.IsAuthenticated returns true now, the access_token is null and subsequent calls on the client fail. I suppose that I need to implement functionality to convert the refresh_token to an access_token somewhere, but I'm wondering if there's an easier way to leverage that code that is already present in the SDK?

ginach commented 8 years ago

You are correct, I forgot that AuthenticateAsync() does a few more steps. There's a way to do it with existing functionality, but it's not straighforward. PR #31 adds some extensions to do this, please take a look.

With the changes you can do something like this to get an authenticated client using refresh token:

await OneDriveClient.GetSlientlyAuthenticatedMicrosoftAccountClient(
    clientId,
    returnUrl,
    scopes,
    refreshToken)
ginach commented 8 years ago

The new package with the refresh token fix is up.

mybluedog commented 8 years ago

@ginach Working well! Please note typo in GetSlientlyAuthenticatedMicrosoftAccountClient :-)

mybluedog commented 8 years ago

@ginach Hi Gina

We had this function working when we were using the well-known redirect_uri (https://login.live.com/oauth20_desktop.srf). Since then, we have switched to using a custom redirect_uri (http://localhost/oauth).

Through experimentation (using REST) we learned that we now need to pass in the client_secret. This means we now need to call the function that accepts a client_secret...

public static Task<IOneDriveClient> GetSlientlyAuthenticatedMicrosoftAccountClient(string appId, string returnUrl, string[] scopes, string clientSecret, string refreshToken, IServiceInfoProvider serviceInfoProvider, CredentialCache credentialCache = null, IHttpProvider httpProvider = null);

...but this function also requires an IServiceInfoProvider (and therefore a ServiceInfo object and an IAuthenticationProvider).

I will attempt to implement these tomorrow, but I don't understand why they are required since REST requires just the first four params to work (string appId, string returnUrl, string[] scopes, string clientSecret).

I tried leaving IServiceInfoProvider as null, but that didn't work. Can you offer some guidance on the minimum implementation needed to get this working? (I don't need code, just an indication on what needs to be provided would be great).

ginach commented 8 years ago

Yup, this is a bug. I had done a lot of testing with the desktop client flow but missed the client secret flow. I have a fix locally and will have an updated package out probably tomorrow.

mybluedog commented 8 years ago

@ginach Any update on this?

ginach commented 8 years ago

Sorry for the delay, I wound up getting sick. Finishing up testing now and will update the package after that.

mybluedog commented 8 years ago

Thanks Gina. I hope you're feeling better.

ginach commented 8 years ago

@mybluedog The new package (v1.1.20) is now uploaded.

mybluedog commented 8 years ago

This is working for us with Consumer accounts now.

byhub commented 8 years ago

Hi @ginach

I havent been able to get authenticated client..I am trying this for asp.net mvc app..

MicrosoftAccountServiceInfo _serviceinfo = new MicrosoftAccountServiceInfo();
_serviceinfo.AppId = settings.ClientId;
_serviceinfo.ClientSecret = settings.ClientSecret;
_serviceinfo.ReturnUrl = returnURL;
_serviceinfo.Scopes = Scopes;
MicrosoftAccountAuthenticationProvider authenticationProvider = new MicrosoftAccountAuthenticationProvider(_serviceinfo);
_client = OneDriveClient.GetMicrosoftAccountClient(settings.ClientId, returnURL, Scopes, settings.ClientSecret, serviceInfoProvider: new ServiceInfoProvider(authenticationProvider)); 
_session = _client.AuthenticateAsync().Result;

I keep getting failed authentication error.. by the way when I check client object, I see that Authentication provider is null.. am I missing something?

ginach commented 8 years ago

@byhub With the way you're initializing the client, whithout an IWebUi instance, it'll hit the silent auth flow but not have a refresh token present to redeem for an access token. This will fail the auth flow.

What you want to do is initialize the client using refresh token. Or create your own IAuthenticationProvider implementation and pass that in to the service info provider. Here's how you can get the client using refresh token:

await OneDriveClient.GetSlientlyAuthenticatedMicrosoftAccountClient(
    clientId,
    returnUrl,
    scopes,
    clientSecret,
    refreshToken);
byhub commented 8 years ago

@ginach hmm probably it will be a silly question but isnt it what I am doing already in my code snippet I shared above.. for IAuthenticationProvider implementation, I use MicrosoftAccountAuthenticationProvider class that is IAuthenticationProvider implementation and pass it into serviceinfoprovider within extensin call..

why do I need to create another IAuthenticationProvider implementation..?

skobba commented 8 years ago

@byhub you are not the only one struggelig, I'm having a hard time using this with "New Project" -> ASP .NET 5 Web Application Template (using multi-domain). Cheers

ginach commented 8 years ago

MSA OAuth requires a user prompt for consent. We have default UI implemented for WinStore apps and an implementation using WinForms to prompt. With ASP.NET web applications this can create an interesting problem as prompting users usually requires switching to a different view context and the default UI implementations aren't sufficient, or the code is running server-side and cannot prompt the user. In these cases, the recommendation is for the app to use either of these flows:

  1. Create a signup page that will have to user do the initial "Connect with OneDrive", making sure to include the wl.offline_access scope so a refresh token will be returned. Then, store those refresh tokens and initialize a client using GetSilentlyAuthenticatedMicrosoftAccountClient(). Note that tokens are valid for a year and a new one will be retrieved anytime an access token is retrieved. So, after authenticating the client you'll want to store the new token.
  2. Handle your own authentication and implement your own IAuthenticationProvider that will append the auth tokens to the request.

I'm currently working on implementing a 3rd option where the app handles its own initial OAuth call using the authorization code grant type and can pass that code to the client initialization. I have it implemented for business auth flows but not yet for consumer.

skobba commented 8 years ago

@ginach, is the business implementation in one of your repo branches, businessAuthFixes?

ginach commented 8 years ago

Correct. It's also the current PR into Dev.

skobba commented 8 years ago

A lot of the auth code is in OneDriveSdk.WindowsForms including the BusinessClientExtensions. Find it difficult to get this running on ASP .NET. I'm trying this with the new ASP .NET Core 1.0 that bootstraps the app with auth middleware in the Startup.cs file. Would it be a good idea to be able to bootstrap the app with OneDriveSDK capabilities? Lots of OneDrive browsers out there like: OneDriveExplorer (Graph/TypeScript) and OneDriveForBusiness-Explorer-MVC. Just trying to make my own OneDriveExplorerASP.NETCore1.0 with your new API. Not sure how to proceed now. By the way, I think you have to update the WinForm sample to include the client_secret. Cheers

ginach commented 8 years ago

There are a few approaches you can take. Please feel free to email me at ginach @ microsoft . com to talk about some approaches and so I can get an idea of how I can improve the calling model here.

ginach commented 8 years ago

Business auth changes are in the latest package push. For an MVC sample, take a look at our webhooks sample app.