Garethp / php-ews

PHP Exchange Web Services
BSD 3-Clause "New" or "Revised" License
112 stars 45 forks source link

Is it possible to get user's Authorization Codes for OAuth, directly from Azure AD and without a web browser redirect? #236

Open paladini opened 1 year ago

paladini commented 1 year ago

Hey @Garethp , I've seen some of your code examples like https://github.com/Garethp/php-ews/blob/master/examples/basic/authenticatingWithOAuth.php and I would like to thank you so much for implementing OAuth for this old Microsoft API (EWS). However, I've a specific case in which I need to do OAuth in behalf of an user from an Azure Active Directory (Azure AD) registered under a specific tenant. It must be done from our back-end application that users has no interactions at all, so I cannot redirect the user to an OAuth permission page like usually we would make. I need to get the users permissions (or the user's authorization token) by other way.

Possibilities

I've seen that we can request authorization code from MSAL client to get tokens (also 2 ). I'm not sure if there's a way to get authenticating codes via MSAL by using EWS or if it's only possible through Graph API.

It's possible to do that from your library? Can you give some instructions or example code? I know that I may be asking too much, but kind of confused yet about all these APIs and ways of retrieving authorization codes to use in OAuth in your library.

Notes

Note: when I'm saying authorization codes that are needed for OAuth, I'm talking about those from this variable 'code': https://github.com/Garethp/php-ews/blob/7920fefb0bb431cb2df0b2ba58d09330535ced6c/examples/basic/authenticatingWithOAuth.php#L38

Garethp commented 1 year ago

This answer may not be helpful, but it may be possible to do what you're asking. I can't comment on MSAL as I've not heard of it before, but the second link is explicitly about EWS so it should be possible to implement the "Get a token with app-only auth" example from it. The problem here is that they're only giving an example in C# with their Managed API which makes it quite difficult to figure out how to do that.

This library acts as somewhat thing wrapper around Microsoft's SOAP service, it's why the Manual Usage example in the README looks the way it does, it gets translated to XML in a very similar shape (This is the documentation for the CreateItem call that the "Manual Usage" section is using). So as long as you can find an XML/SOAP example of doing things, making it work in this library shouldn't be too difficult.

Usually their examples that use the Managed API are also accompanied by similar documentation for their SOAP service, this is a good example of them showing how to do things in both C# and XML.

While the example that you posted doesn't have any XML accompanying it, I would guess that it also uses the SOAP XML service under the hood (though I can't be sure), so there should be a way of making the same calls but I can't tell you how. If you're able to find out how to do it with XML, I can help show you how to do it with this library and maybe even add it as a function to this library.

paladini commented 1 year ago

Thank you so much for this fast reply, @Garethp ! I'm really hurrying here because MS is going to drop the Basic Auth very soon.

This answer may not be helpful, but it may be possible to do what you're asking. I can't comment on MSAL as I've not heard of it before, but the second link is explicitly about EWS so it should be possible to implement the "Get a token with app-only auth" example from it. The problem here is that they're only giving an example in C# with their Managed API which makes it quite difficult to figure out how to do that.

This library acts as somewhat thing wrapper around Microsoft's SOAP service, it's why the Manual Usage example in the README looks the way it does, it gets translated to XML in a very similar shape (This is the documentation for the CreateItem call that the "Manual Usage" section is using). So as long as you can find an XML/SOAP example of doing things, making it work in this library shouldn't be too difficult.

Usually their examples that use the Managed API are also accompanied by similar documentation for their SOAP service, this is a good example of them showing how to do things in both C# and XML.

While the example that you posted doesn't have any XML accompanying it, I would guess that it also uses the SOAP XML service under the hood (though I can't be sure), so there should be a way of making the same calls but I can't tell you how. If you're able to find out how to do it with XML, I can help show you how to do it with this library and maybe even add it as a function to this library.

So currently your implementation for OAuth only works for users giving their explicit permission by a web browser redirect? There's some other way I haven't mentioned here that may help me do the same as from your example (without improving your lib's code) but having the authorization from Azure AD to access resources from all users registered in it?

lilhater commented 1 year ago

How did you go with this @paladini? I would also like a way to get a token without the users being prompted for a login.

j-applese3d commented 1 year ago

I got it to work though I'm not sure what all the different settings on the Azure side were needed. In my case, I'm using EWS to read calendar events for some meeting rooms, so if you are actually trying to login as someone else, this might not be helpful. Anyways here's the PHP code I used:

function getO365Token(): string {
    // If we already have a user token, just return it
    if (file_exists('./token')) {
        $mod = filemtime('./token');
        $val = json_decode(file_get_contents('./token'), true);
        // Tokens are valid for one hour, after that it needs to be refreshed
        if ($mod + getInt($val['expires_in']) > time()) {
            logger("Token is still active.");
            return $val['access_token'];
        }
        logger("Token is expired. getting a new one! $mod + {$val['expires_in']} < " . time());
    }

    // \Curl is a custom wrapper class around php's curl functions.
    // you could achieve the same effect by doing a system `curl -X POST <url> --data '<data>'` call...
    $c = new \Curl(\Curl::TYPE_POST);
    $c->dontDecode();
    $c->setBody(
        "grant_type=client_credentials" .
        "&client_id=" . EWS_AZURE_CLIENT_ID .
        "&client_secret=" . EWS_AZURE_CLIENT_SECRET .
        "&scope=https://outlook.office365.com/.default"
    );
    $c->setUrl("https://login.microsoftonline.com/" . EWS_AZURE_TENANT_ID . "/oauth2/v2.0/token");
    $resp = $c->exec();

    file_put_contents('./token', $resp);
    return json_decode($resp, true)['access_token'];
}

$ews = API::withCallbackToken('outlook.office365.com', getO365Token(), [
    'impersonation' => $emailAddress,
]);
// use like normal
$r = $ews->getClient()->GetUserOofSettings($request);

Hopefully, this is helpful.

muhamadnazmi99 commented 1 year ago

I got it to work though I'm not sure what all the different settings on the Azure side were needed. In my case, I'm using EWS to read calendar events for some meeting rooms, so if you are actually trying to login as someone else, this might not be helpful. Anyways here's the PHP code I used:

function getO365Token(): string {
    // If we already have a user token, just return it
    if (file_exists('./token')) {
        $mod = filemtime('./token');
        $val = json_decode(file_get_contents('./token'), true);
        // Tokens are valid for one hour, after that it needs to be refreshed
        if ($mod + getInt($val['expires_in']) > time()) {
            logger("Token is still active.");
            return $val['access_token'];
        }
        logger("Token is expired. getting a new one! $mod + {$val['expires_in']} < " . time());
    }

    // \Curl is a custom wrapper class around php's curl functions.
    // you could achieve the same effect by doing a system `curl -X POST <url> --data '<data>'` call...
    $c = new \Curl(\Curl::TYPE_POST);
    $c->dontDecode();
    $c->setBody(
        "grant_type=client_credentials" .
        "&client_id=" . EWS_AZURE_CLIENT_ID .
        "&client_secret=" . EWS_AZURE_CLIENT_SECRET .
        "&scope=https://outlook.office365.com/.default"
    );
    $c->setUrl("https://login.microsoftonline.com/" . EWS_AZURE_TENANT_ID . "/oauth2/v2.0/token");
    $resp = $c->exec();

    file_put_contents('./token', $resp);
    return json_decode($resp, true)['access_token'];
}

$ews = API::withCallbackToken('outlook.office365.com', getO365Token(), [
    'impersonation' => $emailAddress,
]);
// use like normal
$r = $ews->getClient()->GetUserOofSettings($request);

Hopefully, this is helpful.

can i see the custom curl class. I dont understand about curl