Using Auth0 in Spring Cloud Gateway, problem with getting ID Token - spring-security

We have problems with obtaining ID token using auth0 SDK.
We have API Gateway based on Spring Cloud Gateway (version 3.1.4) where we try to use your auth0 platform to authenticate the users and then route the exchange to our micro services. To do it we would like to use ID Token and get email from it and pass this email to our micro services.
We log in by hitting oauth2/authorization/auth0 endpoint, we are being redirected to auth0 login page, where we provide credentials, then we get redirect back to our app.
When we configuire endpoints directly in API Gateway and mark them with #AuthenticationPrincipal OidcUser user it works and we have full user details as well as ID token.
When we proxy the exchange to different service we have Authorisation header in the request, which contains only header & signature part without payload in the ID token.
We would need the payload in ID Token in order to fetch user email for mapping the user with our internal DB in our micro services.
What do you think would be the proper workflow in this case and how can we solve this issue?
We tried to use Rules & Actions, which I paste below, but it didn’t helped us.
Our configuration looks like this:
#Bean
public SecurityWebFilterChain filterChain(ServerHttpSecurity http) throws Exception {
return http
.csrf().disable()
.authorizeExchange()
.pathMatchers("/test").authenticated()
.anyExchange().authenticated()
.and().oauth2Login()
.and().logout().logoutSuccessHandler(logoutSuccessHandler())
.and().build();
}
In the RouteLocator in GatewayConfiguration we have filter for TokenRelay.
Our Action looks like this:
exports.onExecutePostLogin = async (event, api) => {
const namespace = 'http://test.{our_local_development_route}:8888';
if (event.authorization) {
api.idToken.setCustomClaim(`${namespace}/claims/email`, event.user.email);
api.accessToken.setCustomClaim(`${namespace}/email`, event.user.email);
}
};
And Rule:
function addEmailToAccessToken(user, context, callback) {
// This rule adds the authenticated user's email address to the access token.
const namespace = 'http://test.{our_local_development_route}:8888';
context.idToken[namespace + 'email'] = user.upn;
context.accessToken[namespace + 'email'] = user.email;
return callback(null, user, context);
}

I recently created a microservices architecture with Spring Cloud Gateway and Auth0. I wrote about how I created it on the Auth0 blog. That's not the interesting part. The interesting part is JHipster generates a ReactiveJwtDecoder that calls a /userinfo endpoint if some claims aren't available in the access token. This way, the access token is enriched with identity information before it's relayed to downstream microservices.
#Bean
ReactiveJwtDecoder jwtDecoder(ReactiveClientRegistrationRepository registrations) {
Mono<ClientRegistration> clientRegistration = registrations.findByRegistrationId("oidc");
return clientRegistration
.map(oidc ->
createJwtDecoder(
oidc.getProviderDetails().getIssuerUri(),
oidc.getProviderDetails().getJwkSetUri(),
oidc.getProviderDetails().getUserInfoEndpoint().getUri()
)
)
.block();
}
private ReactiveJwtDecoder createJwtDecoder(String issuerUri, String jwkSetUri, String userInfoUri) {
NimbusReactiveJwtDecoder jwtDecoder = new NimbusReactiveJwtDecoder(jwkSetUri);
OAuth2TokenValidator<Jwt> audienceValidator = new AudienceValidator(jHipsterProperties.getSecurity().getOauth2().getAudience());
OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuerUri);
OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);
jwtDecoder.setJwtValidator(withAudience);
return new ReactiveJwtDecoder() {
#Override
public Mono<Jwt> decode(String token) throws JwtException {
return jwtDecoder.decode(token).flatMap(jwt -> enrich(token, jwt));
}
private Mono<Jwt> enrich(String token, Jwt jwt) {
// Only look up user information if identity claims are missing
if (jwt.hasClaim("given_name") && jwt.hasClaim("family_name")) {
return Mono.just(jwt);
}
// Retrieve user info from OAuth provider if not already loaded
return users.get(
jwt.getSubject(),
s -> {
WebClient webClient = WebClient.create();
return webClient
.get()
.uri(userInfoUri)
.headers(headers -> headers.setBearerAuth(token))
.retrieve()
.bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {})
.map(userInfo ->
Jwt
.withTokenValue(jwt.getTokenValue())
.subject(jwt.getSubject())
.audience(jwt.getAudience())
.headers(headers -> headers.putAll(jwt.getHeaders()))
.claims(claims -> {
String username = userInfo.get("preferred_username").toString();
// special handling for Auth0
if (userInfo.get("sub").toString().contains("|") && username.contains("#")) {
userInfo.put("email", username);
}
// Allow full name in a name claim - happens with Auth0
if (userInfo.get("name") != null) {
String[] name = userInfo.get("name").toString().split("\\s+");
if (name.length > 0) {
userInfo.put("given_name", name[0]);
userInfo.put("family_name", String.join(" ", Arrays.copyOfRange(name, 1, name.length)));
}
}
claims.putAll(userInfo);
})
.claims(claims -> claims.putAll(jwt.getClaims()))
.build()
);
}
);
}
};
}

Related

JWT Same Application is the Auth Server and the Application Server

Hi experts I have recently explored a lot on OAuth and how JWT works. Essentially there should be an AuthServer that issues a token and there should be a ServiceAPI(Application Server) that uses the token for a client!!. I also understand a token is made up of 3-parts, the header, payload and signature...
Now what if I want to build an API that does both ...Authenticates and issues JWT tokens - and then provide the service afterwards..It Sounds like Basic Authentication with tokens!!
I am also unsure if the code I have written reflects this concept (that token issuer is the same as ServiceAPI). I am building a .net core 2.1 Web API following this article.
In the Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
//Authentication
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.Authority = "https://localhost:44387/";
options.Audience = "JWT:Issuer";
options.TokenValidationParameters.ValidateLifetime = true;
options.TokenValidationParameters.ClockSkew = TimeSpan.FromMinutes(5);
options.RequireHttpsMetadata = false;
});
services.AddAuthorization(options =>
{
options.AddPolicy("GuidelineReader", p => {
p.RequireClaim("[url]", "GuidelineReader");
});
});
}
I have also added a LoginController that generates the token and returns it...
[AllowAnonymous]
[HttpPost]
public IActionResult Login([FromBody]Application login)
{
IActionResult response = Unauthorized();
var user = AuthenticateUser(login);
if (user != null)
{
var tokenString = GenerateJSONWebToken(user);
response = Ok(new { token = tokenString });
}
return response;
}
private string GenerateJSONWebToken(Application appInfo)
{
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]));
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(_config["Jwt:Issuer"],
_config["Jwt:Issuer"],
null,
expires: DateTime.Now.AddMinutes(120),
signingCredentials: credentials);
return new JwtSecurityTokenHandler().WriteToken(token);
}
What is the difference in the following
options.Authority
options.Audience (I am thinking it is the application that sends https request)
options.Issuer
options.Authority
Authority is the address of the token-issuing authentication server. In your scenario the web api issue the token , so the Authority will be the url of web api .
options.Audience (I am thinking it is the application that sends https request)
Audience represents the intended recipient of the incoming token or the resource that the token grants access to. In your scenario ,the web api is the protected resource which client will access with JWT token , web api will validate the token to check the claims/signature . So web api name/URL should be the Audience
options.Issuer
Issuer Identifies the security token service (STS) that constructs and returns the token . In your scenario , the web api validates the user credential and return token. So web api name/URL is the Issuer

Get Authorization code for Azure PowerBI capacity for PowerBI Embedded

I'm programatically start/stop Azure PowerBI capacity for PowerBI Embedded.
On button click , resume/suspend the powerbi embed service in Azure. I followed below link to do this.
https://learn.microsoft.com/en-us/rest/api/power-bi-embedded/capacities/resume
How to get authorization code dynamicallly each time i click the button.
You can get an access token for Power BI using Azure Active Directory Authentication Libraries. The easiest way to get it is to install Microsoft.IdentityModel.Clients.ActiveDirectory NuGet package. Then to obtain an access token you need to call AcquireTokenAsync method. Here is how you can do this:
private static string redirectUri = "https://login.live.com/oauth20_desktop.srf";
private static string resourceUri = "https://analysis.windows.net/powerbi/api";
private static string authorityUri = "https://login.windows.net/common/oauth2/authorize";
// Obtain at https://dev.powerbi.com/apps
private static string clientId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";
private static AuthenticationContext authContext = new AuthenticationContext(authorityUri, new TokenCache());
private async void btnAuthenticate_ClickAsync(object sender, EventArgs e)
{
var authenticationResult = await authContext.AcquireTokenAsync(resourceUri, clientId, new Uri(redirectUri), new PlatformParameters(PromptBehavior.Auto));
if (authenticationResult == null)
MessageBox.Show("Call failed.");
else
MessageBox.Show(authenticationResult.AccessToken);
}
The last parameter is PromptBehavior.Auto. This means that you will be prompted for credentials, unless your identity is saved on this computer. Also, when there is no consent for access is given this app, the user will be prompted too. The authentication is performed in an interactive way - it expect that there will be a human, who will enter credentials in case they are needed. If you want to obtain an access token in non-interactive way, you can use user name and password in your code. In this case the method for obtaining the access token should look like this:
private void btnAuthenticate_Click(object sender, EventArgs e)
{
AuthenticationResult authenticationResult = null;
// First check is there token in the cache
try
{
authenticationResult = authContext.AcquireTokenSilentAsync(resourceUri, clientId).Result;
}
catch (AggregateException ex)
{
AdalException ex2 = ex.InnerException as AdalException;
if ((ex2 == null) || (ex2 != null && ex2.ErrorCode != "failed_to_acquire_token_silently"))
{
MessageBox.Show(ex.Message);
return;
}
}
if (authenticationResult == null)
{
var uc = new UserPasswordCredential("user#example.com", "<EnterStrongPasswordHere>"); // Or parameterless if you want to use Windows integrated auth
try
{
authenticationResult = authContext.AcquireTokenAsync(resourceUri, clientId, uc).Result;
}
catch (Exception ex)
{
MessageBox.Show(ex.Message + ex.InnerException == null ? "" : Environment.NewLine + ex.InnerException.Message);
return;
}
}
if (authenticationResult == null)
MessageBox.Show("Call failed.");
else
MessageBox.Show(authenticationResult.AccessToken);
}
Please note, that this call may fail, if no consent is given to your app. To do this, go to Azure Portal -> Azure Active Directory -> App registrations and locate your app. Then open your app's settings and in Required permissions select Power BI Service and click Grant permissions:
At this point you can use this access token to perform REST API calls or to embed elements in your app. This token gives access to everything which the user can access and it has been allowed to be accessed when you registered your app in the portal. If you want however to generate a token for one particular report (or tile, or dashboard), then you can call some of the Embed Token methods, e.g. GenerateTokenInGroup (using the ADAL access token to authenticate yourself in the headers of the request for generating the embedded token).

Azure Mobile Services LoginAsync method not working with Microsoft Auth Token

I have successfully been able to get an access_token (or authenticationToken for Microsoft tokens) using the client side authentication in my Xamarin forms App. I am able to get further user information (email, name, etc.) using the same access token. Now, when I try to pass that token to my Azure Mobile Service backend, I get a 401 error.
Here is my code:
private async System.Threading.Tasks.Task<string> MSGetUserInfo(Account account)
{
// Reference: http://graph.microsoft.io/en-us/docs/overview/call_api
// Note that Microsoft don't recognize the access_token header entry, but rely instead on an Authorization header entry
var client = new HttpClient();
var userInfoRequest = new HttpRequestMessage()
{
RequestUri = new Uri("https://graph.microsoft.com/v1.0/me"),
Method = HttpMethod.Get,
};
// Add acccess Bearer
userInfoRequest.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", account.Properties["access_token"]);
using (var response = await client.SendAsync(userInfoRequest).ConfigureAwait(false))
{
if (response.IsSuccessStatusCode)
{
Models.User user = new Models.User();
var responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
var jobject = JObject.Parse(responseString);
var userName = (string)jobject["userPrincipalName"];
// Check username is valid
if (String.IsNullOrEmpty(userName))
{
throw new Exception("Username was not set for authenticated user");
}
else
user.ProviderLoginId = userName;
var userDisplayName = (string)jobject["displayName"];
// Replace display name if invalid
if (String.IsNullOrWhiteSpace(userDisplayName))
{
userDisplayName = userName;
}
else
user.Name = userDisplayName;
var userEmail = (string)jobject["mail"];
// Replace email if invalid
if (String.IsNullOrWhiteSpace(userEmail))
{
userEmail = userName;
}
else
user.Email = userEmail;
Valufy.App.currentUser = user;
}
else
{
throw new Exception("OAuth2 request failed: " + await response.Content.ReadAsStringAsync().ConfigureAwait(false));
}
}
return "success";
}
The above code snippet works in getting my user details. Now when I try to use the same token in the subsequent call, I get a 404:
public async Task<bool> Authenticate(string token)
{
string message = string.Empty;
var success = false;
JObject objToken = new JObject();
//objToken.Add("access_token", token); //for facebook and google
objToken.Add("authenticationToken", token); //for microsoft
try
{
// Sign in with Facebook login using a server-managed flow.
if (user == null)
{
//ProviderAuth("MICROSOFT");
user = await syncMgr.CurrentClient
.LoginAsync(MobileServiceAuthenticationProvider.MicrosoftAccount, objToken);
if (user != null)
{
success = true;
message = string.Format("You are now signed-in as {0}.", user.UserId);
}
}
}
catch (Exception ex)
{
message = string.Format("Authentication Failed: {0}", ex.Message);
}
// Display the success or failure message.
// await new MessageDialog(message, "Sign-in result").ShowAsync();
return success;
}
Is there something that I am doing wrong? Any and all assistance is appreciated.
According to your description, I followed this Git sample about Microsoft Graph Connect Sample for UWP (REST). I could get the access_token and it could work as expected with Microsoft Graph API (e.g. Get a user). But when I use this access_token as the authenticationToken token object for MobileServiceClient.LoginAsync, I could also get 401 Unauthorized.
Then I checked the managed client for Azure Mobile Apps about Authenticate users. For Client-managed authentication flow, I found that the official code sample about using Microsoft Account is working with Live SDK as follows:
// Request the authentication token from the Live authentication service.
// The wl.basic scope should always be requested. Other scopes can be added
LiveLoginResult result = await liveIdClient.LoginAsync(new string[] { "wl.basic" });
if (result.Status == LiveConnectSessionStatus.Connected)
{
session = result.Session;
// Get information about the logged-in user.
LiveConnectClient client = new LiveConnectClient(session);
LiveOperationResult meResult = await client.GetAsync("me");
// Use the Microsoft account auth token to sign in to App Service.
MobileServiceUser loginResult = await App.MobileService
.LoginWithMicrosoftAccountAsync(result.Session.AuthenticationToken);
}
Note: As LiveConnectSession states about AuthenticationToken:
The authentication token for a signed-in and connected user.
While check the authentication with Microsoft Graph, I could only find the access_token instead of AuthenticationToken.
UPDATE:
I have checked LiveLogin for WP8 and Microsoft Account Authentication for Mobile Apps via Fiddler to capture the authorize requests. I found that MS account authentication has the similar authorize request as Live SDK.
I assumed that you need to leverage Live SDK to authenticate the user when using client side authentication with Microsoft account. I found the Live SDK download page is not exist, you could follow the Live SDK for WP8 to get started with Live SDK.
UPDATE2:
For the client-flow authentication (Microsoft Account), you could leverage MobileServiceClient.LoginWithMicrosoftAccountAsync("{Live-SDK-session-authentication-token}"), also you could use LoginAsync with the token parameter of the value {"access_token":"{the_access_token}"} or {"authenticationToken":"{Live-SDK-session-authentication-token}"}. I have tested LoginAsync with the access_token from MSA and retrieve the logged info as follows:

OpenID Connect server with ASOS, .NET Core pipeline

I have started playing with OpenID Connect server with ASOS by implementing the resource owner password credential grant. however when I test it using postman, I am getting generic 500 internal server error.
Here is my code for your debugging pleasure. I appreciate your feedback.
Thanks
-Biruk
here is my Startup.cs
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddAuthentication(options => {
options.SignInScheme = "ServerCookie";
});
services.AddApplicationInsightsTelemetry(Configuration);
services.AddMvc();
services.AddSession(options => {
options.IdleTimeout = TimeSpan.FromMinutes(30);
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, LoggerFactory loggerFactory)
{
app.UseOAuthValidation();
app.UseOpenIdConnectServer(options => {
// Create your own authorization provider by subclassing
// the OpenIdConnectServerProvider base class.
options.Provider = new AuthorizationProvider();
// Enable the authorization and token endpoints.
// options.AuthorizationEndpointPath = "/connect/authorize";
options.TokenEndpointPath = "/connect/token";
// During development, you can set AllowInsecureHttp
// to true to disable the HTTPS requirement.
options.ApplicationCanDisplayErrors = true;
options.AllowInsecureHttp = true;
// Note: uncomment this line to issue JWT tokens.
// options.AccessTokenHandler = new JwtSecurityTokenHandler();
});
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
app.UseApplicationInsightsRequestTelemetry();
app.UseApplicationInsightsExceptionTelemetry();
app.UseMvc();
}
and here is my AuthorizationProvider.cs
public sealed class AuthorizationProvider : OpenIdConnectServerProvider
{
public Task<User> GetUser()
{
return Task.Run(()=> new User { UserName = "biruk60", Password = "adminUser123" });
}
// Implement OnValidateAuthorizationRequest to support interactive flows (code/implicit/hybrid).
public override Task ValidateTokenRequest(ValidateTokenRequestContext context)
{
// Reject the token request that don't use grant_type=password or grant_type=refresh_token.
if (!context.Request.IsPasswordGrantType() && !context.Request.IsRefreshTokenGrantType())
{
context.Reject(
error: OpenIdConnectConstants.Errors.UnsupportedGrantType,
description: "Only resource owner password credentials and refresh token " +
"are accepted by this authorization server");
return Task.FromResult(0);
}
// Since there's only one application and since it's a public client
// (i.e a client that cannot keep its credentials private), call Skip()
// to inform the server the request should be accepted without
// enforcing client authentication.
context.Skip();
return Task.FromResult(0);
}
public override async Task HandleTokenRequest(HandleTokenRequestContext context)
{
//// Resolve ASP.NET Core Identity's user manager from the DI container.
//var manager = context.HttpContext.RequestServices.GetRequiredService<UserManager<ApplicationUser>>();
// Only handle grant_type=password requests and let ASOS
// process grant_type=refresh_token requests automatically.
if (context.Request.IsPasswordGrantType())
{
// var user = await manager.FindByNameAsync(context.Request.Username);
var user = await GetUser();//new { userName = "briuk60#gmail.com", password = "adminUser123" };
if (user == null)
{
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidGrant,
description: "Invalid credentials.");
return;
}
if (user != null && (user.Password == context.Request.Password))
{
var identity = new ClaimsIdentity(context.Options.AuthenticationScheme);
// Note: the name identifier is always included in both identity and
// access tokens, even if an explicit destination is not specified.
// identity.AddClaim(ClaimTypes.NameIdentifier, await manager.GetUserId(user));
// When adding custom claims, you MUST specify one or more destinations.
// Read "part 7" for more information about custom claims and scopes.
identity.AddClaim("username", "biruk60",
OpenIdConnectConstants.Destinations.AccessToken,
OpenIdConnectConstants.Destinations.IdentityToken);
// Create a new authentication ticket holding the user identity.
var ticket = new AuthenticationTicket(
new ClaimsPrincipal(identity),
new AuthenticationProperties(),
context.Options.AuthenticationScheme);
// Set the list of scopes granted to the client application.
ticket.SetScopes(
/* openid: */ OpenIdConnectConstants.Scopes.OpenId,
/* email: */ OpenIdConnectConstants.Scopes.Email,
/* profile: */ OpenIdConnectConstants.Scopes.Profile);
// Set the resource servers the access token should be issued for.
// ticket.SetResources("resource_server");
context.Validate(ticket);
}
}
}
}
What am i doing wrong. I can put it in debug mode and step through it without any error it just 500 internal Server Error in fiddler and postman.
Here's the exception you're likely seeing:
System.InvalidOperationException: A unique identifier cannot be found to generate a 'sub' claim: make sure to add a 'ClaimTypes.NameIdentifier' claim.
Add a ClaimTypes.NameIdentifier claim and it should work.

I want to use office 365 Api for my Asp.net with C# non mvc Project

Hello Folks I am new to using API into my project.
I am using Asp.Net with C# which does not have MVC architecture.
My client needed to integrate office 365 API into the project so that any user who want to access our service can login through their office 365 credentials.
while I searched on internet about the sources it said I needed ASP.net with MVC to use office 365 . Please suggest what could be done.
You could use Active Directory Authentication Library .NET to easily authenticate users to cloud or on-premises Active Directory (AD), and then obtain access tokens for securing API calls . In web form , code below is for your reference :
protected void Page_Load(object sender, EventArgs e)
{
string authCode = Request.Params["code"];
if (!string.IsNullOrEmpty(authCode))
{
Authorize(authCode);
}
string token = (string)Session["access_token"];
if (string.IsNullOrEmpty(token))
{
return;
}
try
{
// get user name
getUserName(token);
}
catch (AdalException ex)
{
}
}
public void getUserName(string token)
{
using (var client = new HttpClient())
{
//Enable signon and read users' profile
using (var request = new HttpRequestMessage(HttpMethod.Get, "https://graph.microsoft.com/beta/me"))
{
request.Headers.Add("Authorization", "Bearer " + token);
request.Headers.Add("Accept", "application/json;odata.metadata=minimal");
using (var response = client.SendAsync(request).Result)
{
if (response.StatusCode == HttpStatusCode.OK)
{
var json = JObject.Parse(response.Content.ReadAsStringAsync().Result);
Response.Write(json["displayName"].ToString());
}
}
}
}
}
public void Authorize(string authCode) {
AuthenticationContext authContext = new AuthenticationContext("https://login.microsoftonline.com/common");
// The same url we specified in the auth code request
string redirectUri = "http://localhost:55065/Default.aspx";
// Use client ID and secret to establish app identity
ClientCredential credential = new ClientCredential(ConfigurationManager.AppSettings["ClientID"], ConfigurationManager.AppSettings["ClientSecret"]);
try
{
// Get the token
var authResult = authContext.AcquireTokenByAuthorizationCode(
authCode, new Uri(redirectUri), credential, "https://graph.microsoft.com/");
// Save the token in the session
Session["access_token"] = authResult.AccessToken;
Response.Redirect(redirectUri.ToString());
}
catch (AdalException ex)
{
//return Content(string.Format("ERROR retrieving token: {0}", ex.Message));
}
}
public void signin()
{
var authContext = new AuthenticationContext("https://login.microsoftonline.com/common");
// The url in our app that Azure should redirect to after successful signin
string redirectUri = "http://localhost:55065/Default.aspx";
// Generate the parameterized URL for Azure signin
Uri authUri = authContext.GetAuthorizationRequestURL("https://graph.microsoft.com/", ConfigurationManager.AppSettings["ClientID"],
new Uri(redirectUri), UserIdentifier.AnyUser, null);
// Redirect the browser to the Azure signin page
Response.Redirect(authUri.ToString());
}
You could also refer to below link to get some examples with O365 API in GitHub :
https://github.com/dream-365/OfficeDev-Samples/tree/master/samples/Office365DevQuickStart
But I would suggest you could try to use ASP.NET MVC , it is designed with separation of concerns and testability in mind , and you will find a lot of MVC samples with O365 in here:
Office 365 API code samples and videos

Resources